diff --git a/nixos/doc/manual/release-notes/rl-2611.section.md b/nixos/doc/manual/release-notes/rl-2611.section.md index 3697b3bb2e3e7..3a00b506896f4 100644 --- a/nixos/doc/manual/release-notes/rl-2611.section.md +++ b/nixos/doc/manual/release-notes/rl-2611.section.md @@ -28,6 +28,8 @@ - Python 2 has been removed from the top-level package set, as it is long past end-of-life. The `python2`, `python27`, `python2Full`, `python27Full`, `python2Packages`, and `python27Packages` attributes, along with the legacy `python`, `pythonFull`, and `pythonPackages` aliases, now throw an error directing you to `python3`. The `isPy2` and `isPy27` package flags have been removed accordingly. The only remaining Python 2 interpreter is vendored inside the `resholve` package for its `oil` dependency and is not exposed for general use. +- `security.polkit.enablePkexecWrapper` has been introduced, making the `pkexec` setuid wrapper opt-in. + - `systemd.user.extraConfig` has been removed in favor of the structured [](#opt-systemd.user.settings.Manager) option. Use `systemd.user.settings.Manager` to set any `systemd-user.conf(5)` option directly. For example, replace `systemd.user.extraConfig = "DefaultTimeoutStartSec=60";` with `systemd.user.settings.Manager.DefaultTimeoutStartSec = 60;`. - `services.timesyncd.extraConfig` has been removed in favor of the structured [](#opt-services.timesyncd.settings.Time) option. Use `services.timesyncd.settings.Time` to set any `timesyncd.conf(5)` option directly. For example, replace `services.timesyncd.extraConfig = "PollIntervalMaxSec=180";` with `services.timesyncd.settings.Time.PollIntervalMaxSec = 180;`. diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares.nix index 097a4eca51d5c..59e050fe28462 100644 --- a/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares.nix +++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares.nix @@ -11,6 +11,9 @@ in { imports = [ ./installation-cd-graphical-base.nix ]; + # required for calamares + security.polkit.enablePkexecWrapper = true; + # required for kpmcore to work correctly programs.partition-manager.enable = true; diff --git a/nixos/modules/installer/tools/tools.nix b/nixos/modules/installer/tools/tools.nix index 8fd268ee0d2d9..23a0bebe388cb 100644 --- a/nixos/modules/installer/tools/tools.nix +++ b/nixos/modules/installer/tools/tools.nix @@ -330,7 +330,7 @@ in ''; config = lib.mkIf config.system.tools.nixos-rebuild.enableRun0Elevation { - security.polkit.enable = lib.mkDefault true; + security.run0.enable = lib.mkDefault true; environment.systemPackages = [ pkgs.polkit-stdin-agent ]; }; } diff --git a/nixos/modules/programs/gamemode.nix b/nixos/modules/programs/gamemode.nix index 834ae6f54a1d3..3e568f9a68aae 100644 --- a/nixos/modules/programs/gamemode.nix +++ b/nixos/modules/programs/gamemode.nix @@ -60,7 +60,10 @@ in }; security = { - polkit.enable = true; + polkit = { + enable = true; + enablePkexecWrapper = lib.mkDefault true; + }; wrappers = lib.mkIf cfg.enableRenice { gamemoded = { owner = "root"; diff --git a/nixos/modules/programs/throne.nix b/nixos/modules/programs/throne.nix index 2023055892dd0..a64178c777572 100644 --- a/nixos/modules/programs/throne.nix +++ b/nixos/modules/programs/throne.nix @@ -64,32 +64,36 @@ in # 3. Put ThroneCore into a systemd service, and let polkit check service name. # This is the most secure and convenient way but requires heavy modification # to Throne source code. Would be good to let upstream support that eventually. - security.polkit.extraConfig = - lib.mkIf (cfg.tunMode.enable && (!cfg.tunMode.setuid) && config.services.resolved.enable) - '' - polkit.addRule(function(action, subject) { - const allowedActionIds = [ - "org.freedesktop.resolve1.revert", - "org.freedesktop.resolve1.set-domains", - "org.freedesktop.resolve1.set-default-route", - "org.freedesktop.resolve1.set-dns-servers" - ]; + security.polkit = { + enable = true; + enablePkexecWrapper = lib.mkDefault true; + extraConfig = + lib.mkIf (cfg.tunMode.enable && (!cfg.tunMode.setuid) && config.services.resolved.enable) + '' + polkit.addRule(function(action, subject) { + const allowedActionIds = [ + "org.freedesktop.resolve1.revert", + "org.freedesktop.resolve1.set-domains", + "org.freedesktop.resolve1.set-default-route", + "org.freedesktop.resolve1.set-dns-servers" + ]; - if (allowedActionIds.indexOf(action.id) !== -1) { - try { - var parentPid = polkit.spawn(["${lib.getExe' pkgs.procps "ps"}", "-o", "ppid=", subject.pid]).trim(); - var parentCap = polkit.spawn(["${lib.getExe' pkgs.libcap "getpcaps"}", parentPid]).trim(); - if (parentCap.includes("cap_net_admin") && parentCap.includes("cap_net_raw")) { - return polkit.Result.YES; - } else { + if (allowedActionIds.indexOf(action.id) !== -1) { + try { + var parentPid = polkit.spawn(["${lib.getExe' pkgs.procps "ps"}", "-o", "ppid=", subject.pid]).trim(); + var parentCap = polkit.spawn(["${lib.getExe' pkgs.libcap "getpcaps"}", parentPid]).trim(); + if (parentCap.includes("cap_net_admin") && parentCap.includes("cap_net_raw")) { + return polkit.Result.YES; + } else { + return polkit.Result.NOT_HANDLED; + } + } catch (e) { return polkit.Result.NOT_HANDLED; } - } catch (e) { - return polkit.Result.NOT_HANDLED; } - } - }) - ''; + }) + ''; + }; }; meta.maintainers = with lib.maintainers; [ aleksana ]; diff --git a/nixos/modules/security/polkit.nix b/nixos/modules/security/polkit.nix index c2bbe706529f0..6ea840d4ccf39 100644 --- a/nixos/modules/security/polkit.nix +++ b/nixos/modules/security/polkit.nix @@ -6,27 +6,52 @@ }: let - cfg = config.security.polkit; + inherit (lib) + mkEnableOption + mkOption + mkIf + mkPackageOption + mkRemovedOptionModule + types + ; + cfg = config.security.polkit; in { + imports = [ + (mkRemovedOptionModule [ "security" "polkit" "debug" ] "Use security.polkit.extraArgs instead") + ]; + + options.security.polkit = { + enable = mkEnableOption "polkit"; - options = { + enablePkexecWrapper = mkEnableOption "the setuid pkexec wrapper"; - security.polkit.enable = lib.mkEnableOption "polkit"; + package = mkPackageOption pkgs "polkit" { }; - security.polkit.package = lib.mkPackageOption pkgs "polkit" { }; + extraArgs = mkOption { + type = types.listOf types.str; + default = [ + "--no-debug" + "--log-level=notice" + ]; + description = '' + List of arguments to pass to the polkitd executable. - security.polkit.debug = lib.mkEnableOption "debug logs from polkit. This is required in order to see log messages from rule definitions"; + ::: {.note} + To see debug logs you need to negate the default `--no-debug` setting. + ::: + ''; + }; - security.polkit.extraConfig = lib.mkOption { - type = lib.types.lines; + extraConfig = mkOption { + type = types.lines; default = ""; example = '' /* Log authorization checks. */ polkit.addRule(function(action, subject) { - // Make sure to set { security.polkit.debug = true; } in configuration.nix + // Make sure to negate --no-debug in services.polkit.extraArgs: { security.polkit.extraArgs = [ "--log-level=notice" ]; } polkit.log("user " + subject.user + " is attempting action " + action.id + " from PID " + subject.pid); }); @@ -41,8 +66,8 @@ in ''; }; - security.polkit.adminIdentities = lib.mkOption { - type = lib.types.listOf lib.types.str; + adminIdentities = mkOption { + type = with types; listOf str; default = [ "unix-group:wheel" ]; example = [ "unix-user:alice" @@ -58,25 +83,34 @@ in }; - config = lib.mkIf cfg.enable { + config = mkIf cfg.enable { environment.systemPackages = [ cfg.package.bin cfg.package.out ]; - systemd.packages = [ cfg.package.out ]; + services.dbus.packages = [ cfg.package.out ]; - systemd.services.polkit.serviceConfig.ExecStart = [ - "" - "${cfg.package.out}/lib/polkit-1/polkitd ${lib.optionalString (!cfg.debug) "--no-debug"}" - ]; + systemd.packages = [ cfg.package.out ]; - systemd.services.polkit.restartTriggers = [ config.system.path ]; - systemd.services.polkit.reloadTriggers = [ - config.environment.etc."polkit-1/rules.d/10-nixos.rules".source - ]; - systemd.services.polkit.stopIfChanged = false; + systemd.services.polkit = { + restartTriggers = [ config.system.path ]; + reloadTriggers = [ + config.environment.etc."polkit-1/rules.d/10-nixos.rules".source + ]; + serviceConfig.ExecStart = [ + # nuke default ExecStart + "" + # provide our own instead + (toString ( + [ + "${lib.getLib cfg.package}/lib/polkit-1/polkitd" + ] + ++ cfg.extraArgs + )) + ]; + }; systemd.sockets."polkit-agent-helper".wantedBy = [ "sockets.target" ]; @@ -89,7 +123,7 @@ in # The upstream unit uses PrivateDevices=yes and ProtectHome=yes, # which prevents PAM modules from accessing hardware (e.g. FIDO # tokens via /dev/hidraw*) or reading key files from home directories. - (lib.mkIf config.security.pam.u2f.enable { + (mkIf config.security.pam.u2f.enable { # Override upstream PrivateDevices=yes to allow access to /dev/hidraw* PrivateDevices = false; DeviceAllow = [ @@ -100,7 +134,7 @@ in # ~/.config/Yubico/u2f_keys (the default key file location) ProtectHome = "read-only"; }) - (lib.mkIf config.security.pam.zfs.enable { + (mkIf config.security.pam.zfs.enable { PrivateDevices = false; DeviceAllow = [ "/dev/zfs rw" @@ -120,23 +154,16 @@ in ${cfg.extraConfig} ''; # TODO: validation on compilation (at least against typos) - services.dbus.packages = [ cfg.package.out ]; - security.pam.services.polkit-1 = { }; security.wrappers.pkexec = { + enable = cfg.enablePkexecWrapper; setuid = true; owner = "root"; group = "root"; - source = "${cfg.package.bin}/bin/pkexec"; + source = lib.getExe' cfg.package "pkexec"; }; - systemd.tmpfiles.rules = [ - # Probably no more needed, clean up - "R /var/lib/polkit-1" - "R /var/lib/PolicyKit" - ]; - users.users.polkituser = { description = "PolKit daemon"; uid = config.ids.uids.polkituser; diff --git a/nixos/modules/security/run0.nix b/nixos/modules/security/run0.nix index 22296a76c95e4..6aa7f9ad31554 100644 --- a/nixos/modules/security/run0.nix +++ b/nixos/modules/security/run0.nix @@ -6,6 +6,13 @@ }: let + inherit (lib) + mkEnableOption + mkIf + mkMerge + mkOption + ; + cfg = config.security.run0; sudoAlias = pkgs.writeShellScriptBin "sudo" '' @@ -18,7 +25,9 @@ let in { options.security.run0 = { - wheelNeedsPassword = lib.mkOption { + enable = mkEnableOption "support for run0"; + + wheelNeedsPassword = mkOption { type = lib.types.bool; default = true; description = '' @@ -27,26 +36,45 @@ in ''; }; - enableSudoAlias = lib.mkEnableOption "make {command}`sudo` an alias to {command}`run0`."; + enableSudoAlias = mkEnableOption "make {command}`sudo` an alias to {command}`run0`."; }; - config = { - assertions = [ - { - assertion = - cfg.enableSudoAlias -> (!config.security.sudo.enable && !config.security.sudo-rs.enable); - message = "`security.run0.enableSudoAlias` cannot be enabled if `security.sudo` or `security.sudo-rs` are enabled."; - } - ]; - - security.polkit.extraConfig = lib.mkIf (!cfg.wheelNeedsPassword) '' - polkit.addRule(function(action, subject) { - if (action.id == "org.freedesktop.systemd1.manage-units" && subject.isInGroup("wheel")) { - return polkit.Result.YES; + config = mkMerge [ + { + # Late introduction of the enable toggle, this should help during migration. + # TODO: Remove after 26.11 release + assertions = [ + { + assertion = !cfg.wheelNeedsPassword -> cfg.enable; + message = "`security.run0.enable` is currently disabled, but is required for the `security.run0.wheelNeedsPassword` option to take effect"; + } + { + assertion = cfg.enableSudoAlias -> cfg.enable; + message = "`security.run0.enableSudoAlias` depends on `security.run0.enable`, which is disabled."; } - }); - ''; + ]; + } + (mkIf cfg.enable { + assertions = [ + { + assertion = + cfg.enableSudoAlias -> (!config.security.sudo.enable && !config.security.sudo-rs.enable); + message = "`security.run0.enableSudoAlias` cannot be enabled if `security.sudo` or `security.sudo-rs` are enabled."; + } + ]; - environment.systemPackages = lib.optional cfg.enableSudoAlias sudoAlias; - }; + security.polkit = { + enable = true; + extraConfig = mkIf (!cfg.wheelNeedsPassword) '' + polkit.addRule(function(action, subject) { + if (action.id == "org.freedesktop.systemd1.manage-units" && subject.isInGroup("wheel")) { + return polkit.Result.YES; + } + }); + ''; + }; + + environment.systemPackages = lib.optional cfg.enableSudoAlias sudoAlias; + }) + ]; } diff --git a/nixos/modules/services/desktop-managers/budgie.nix b/nixos/modules/services/desktop-managers/budgie.nix index 29bdb469d7f15..807d6b019b729 100644 --- a/nixos/modules/services/desktop-managers/budgie.nix +++ b/nixos/modules/services/desktop-managers/budgie.nix @@ -243,6 +243,8 @@ in # Required by Budgie's Polkit Dialog. security.polkit.enable = mkDefault true; + # Required by Budige's Control Center and Desktop + security.polkit.enablePkexecWrapper = mkDefault true; # Required by Budgie Panel plugins and/or Budgie Control Center panels. networking.networkmanager.enable = mkDefault true; # for BCC's Network panel. diff --git a/nixos/modules/services/desktop-managers/cosmic.nix b/nixos/modules/services/desktop-managers/cosmic.nix index c780a5164922e..3d367f01defe4 100644 --- a/nixos/modules/services/desktop-managers/cosmic.nix +++ b/nixos/modules/services/desktop-managers/cosmic.nix @@ -146,7 +146,10 @@ in environment.sessionVariables.X11_EXTRA_RULES_XML = "${config.services.xserver.xkb.dir}/rules/base.extras.xml"; programs.dconf.enable = true; programs.dconf.packages = [ pkgs.cosmic-session ]; - security.polkit.enable = true; + security.polkit = { + enable = true; + enablePkexecWrapper = lib.mkDefault true; + }; security.rtkit.enable = true; services.accounts-daemon.enable = true; services.displayManager.sessionPackages = [ pkgs.cosmic-session ]; diff --git a/nixos/modules/services/desktop-managers/gnome.nix b/nixos/modules/services/desktop-managers/gnome.nix index 3747a491098f8..1d850f1f26e5a 100644 --- a/nixos/modules/services/desktop-managers/gnome.nix +++ b/nixos/modules/services/desktop-managers/gnome.nix @@ -325,7 +325,11 @@ in i18n.inputMethod.enable = mkDefault true; i18n.inputMethod.type = mkDefault "ibus"; programs.dconf.enable = true; - security.polkit.enable = true; + security.polkit = { + enable = true; + # Required by gnome-initial-setup, gnome-system-monitor, gvfs for admin:// + enablePkexecWrapper = lib.mkDefault true; + }; security.rtkit.enable = mkDefault true; services.accounts-daemon.enable = true; services.dleyna.enable = mkDefault true; diff --git a/nixos/modules/services/desktops/gnome/gnome-remote-desktop.nix b/nixos/modules/services/desktops/gnome/gnome-remote-desktop.nix index 958fbb546dc33..eaa9399862a4e 100644 --- a/nixos/modules/services/desktops/gnome/gnome-remote-desktop.nix +++ b/nixos/modules/services/desktops/gnome/gnome-remote-desktop.nix @@ -22,6 +22,10 @@ config = lib.mkIf config.services.gnome.gnome-remote-desktop.enable { services.pipewire.enable = true; services.dbus.packages = [ pkgs.gnome-remote-desktop ]; + security.polkit = { + enable = true; + enablePkexecWrapper = lib.mkDefault true; + }; environment.systemPackages = [ pkgs.gnome-remote-desktop ]; diff --git a/nixos/modules/services/hardware/tuned.nix b/nixos/modules/services/hardware/tuned.nix index 65a857f4fde85..f399dc55c306c 100644 --- a/nixos/modules/services/hardware/tuned.nix +++ b/nixos/modules/services/hardware/tuned.nix @@ -246,7 +246,10 @@ in systemPackages = [ cfg.package ]; }; - security.polkit.enable = lib.mkDefault true; + security.polkit = { + enable = lib.mkDefault true; + enablePkexecWrapper = lib.mkDefault true; + }; services = { dbus.packages = [ cfg.package ]; diff --git a/nixos/modules/services/x11/desktop-managers/cinnamon.nix b/nixos/modules/services/x11/desktop-managers/cinnamon.nix index a91fa045aa7ae..738aaa000a799 100644 --- a/nixos/modules/services/x11/desktop-managers/cinnamon.nix +++ b/nixos/modules/services/x11/desktop-managers/cinnamon.nix @@ -111,7 +111,10 @@ in services.blueman.enable = mkDefault (notExcluded pkgs.blueman); services.hardware.bolt.enable = mkDefault (notExcluded pkgs.bolt); hardware.bluetooth.enable = mkDefault true; - security.polkit.enable = true; + security.polkit = { + enable = true; + enablePkexecWrapper = lib.mkDefault true; + }; services.accounts-daemon.enable = true; services.system-config-printer.enable = (mkIf config.services.printing.enable (mkDefault true)); services.dbus.packages = with pkgs; [ diff --git a/nixos/modules/services/x11/desktop-managers/xfce.nix b/nixos/modules/services/x11/desktop-managers/xfce.nix index 2d53eb58f2877..3dcc5431e7c8f 100644 --- a/nixos/modules/services/x11/desktop-managers/xfce.nix +++ b/nixos/modules/services/x11/desktop-managers/xfce.nix @@ -220,7 +220,10 @@ in # Enable helpful DBus services. services.udisks2.enable = true; - security.polkit.enable = true; + security.polkit = { + enable = true; + enablePkexecWrapper = lib.mkDefault true; + }; services.accounts-daemon.enable = true; services.upower.enable = config.powerManagement.enable; services.gnome.glib-networking.enable = true; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 5b17b838e3436..3abcd3a653b79 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -408,25 +408,25 @@ in corerad = runTest ./corerad.nix; corteza = runTest ./corteza.nix; cosmic = runTest { - imports = [ ./cosmic.nix ]; + imports = [ ./cosmic ]; _module.args.testName = "cosmic"; _module.args.enableAutologin = false; _module.args.enableXWayland = true; }; cosmic-autologin = runTest { - imports = [ ./cosmic.nix ]; + imports = [ ./cosmic ]; _module.args.testName = "cosmic-autologin"; _module.args.enableAutologin = true; _module.args.enableXWayland = true; }; cosmic-autologin-noxwayland = runTest { - imports = [ ./cosmic.nix ]; + imports = [ ./cosmic ]; _module.args.testName = "cosmic-autologin-noxwayland"; _module.args.enableAutologin = true; _module.args.enableXWayland = false; }; cosmic-noxwayland = runTest { - imports = [ ./cosmic.nix ]; + imports = [ ./cosmic ]; _module.args.testName = "cosmic-noxwayland"; _module.args.enableAutologin = false; _module.args.enableXWayland = false; @@ -1700,6 +1700,7 @@ in tiddlywiki = runTest ./tiddlywiki.nix; tigervnc = handleTest ./tigervnc.nix { }; tika = runTest ./tika.nix; + timekpr = runTest ./timekpr.nix; timezone = runTest ./timezone.nix; timidity = handleTestOn [ "aarch64-linux" "x86_64-linux" ] ./timidity { }; tinc = handleTest ./tinc { }; diff --git a/nixos/tests/cosmic.nix b/nixos/tests/cosmic.nix deleted file mode 100644 index f7fde6ca0e0ff..0000000000000 --- a/nixos/tests/cosmic.nix +++ /dev/null @@ -1,147 +0,0 @@ -{ - config, - lib, - testName, - enableAutologin, - enableXWayland, - ... -}: - -{ - name = testName; - - meta.maintainers = lib.teams.cosmic.members; - - nodes.machine = { - imports = [ ./common/user-account.nix ]; - - services = { - # For `cosmic-store` to be added to `environment.systemPackages` - # and for it to work correctly because Flatpak is a runtime - # dependency of `cosmic-store`. - flatpak.enable = true; - - displayManager.cosmic-greeter.enable = true; - desktopManager.cosmic = { - enable = true; - xwayland.enable = enableXWayland; - }; - }; - - services.displayManager.autoLogin = lib.mkIf enableAutologin { - enable = true; - user = "alice"; - }; - - environment.systemPackages = with config.node.pkgs; [ - # These two packages are used to check if a window was opened - # under the COSMIC session or not. Kinda important. - # TODO: Move the check from the test module to - # `nixos/lib/test-driver/src/test_driver/machine.py` so more - # Wayland-only testing can be done using the existing testing - # infrastructure. - jq - lswt - ]; - - # So far, all COSMIC tests launch a few GUI applications. In doing - # so, the default allocated memory to the guest of 1024M quickly - # poses a very high risk of an OOM-shutdown which is worse than an - # OOM-kill. Because now, the test failed, but not for a genuine - # reason, but an OOM-shutdown. That's an inconclusive failure - # which might possibly mask an actual failure. Not enabling - # systemd-oomd because we need said applications running for a - # few seconds. So instead, bump the allocated memory to the guest - # from 1024M to 4x; 4096M. - virtualisation.memorySize = 4096; - }; - - testScript = - { nodes, ... }: - let - cfg = nodes.machine; - user = cfg.users.users.alice; - DISPLAY = lib.strings.optionalString enableXWayland ( - if enableAutologin then "DISPLAY=:0" else "DISPLAY=:1" - ); - emptyPDF = config.node.pkgs.stdenvNoCC.mkDerivation { - name = "empty-pdf"; - dontUnpack = true; - nativeBuildInputs = [ config.node.pkgs.imagemagick ]; - buildPhase = '' - magick xc:none -page Letter empty.pdf - ''; - installPhase = '' - mkdir $out - mv empty.pdf $out/empty.pdf - ''; - }; - in - '' - #testName: ${testName} - '' - + ( - if enableAutologin then - '' - with subtest("cosmic-greeter initialisation"): - machine.wait_for_unit("graphical.target", timeout=120) - '' - else - '' - from time import sleep - - machine.wait_for_unit("graphical.target", timeout=120) - machine.wait_until_succeeds("pgrep --uid ${toString cfg.users.users.cosmic-greeter.name} --full cosmic-greeter", timeout=30) - # Sleep for 10 seconds for ensuring that `greetd` loads the - # password prompt for the login screen properly. - sleep(10) - - with subtest("cosmic-session login"): - machine.send_chars("${user.password}\n", delay=0.2) - '' - ) - + '' - # _One_ of the final processes to start as part of the - # `cosmic-session` target is the Workspaces applet. So, wait - # for it to start. The process existing means that COSMIC - # now handles any opened windows from now on. - machine.wait_until_succeeds("pgrep --uid ${toString user.uid} --full 'cosmic-panel-button com.system76.CosmicWorkspaces'", timeout=30) - - # The best way to test for Wayland and XWayland is to launch - # the GUI applications and see the results yourself. - with subtest("Launch applications"): - # key: binary_name - # value: "app-id" as reported by `lswt` - gui_apps_to_launch = {} - - # We want to ensure that the first-party applications - # start/launch properly. - gui_apps_to_launch['cosmic-edit'] = 'com.system76.CosmicEdit' - gui_apps_to_launch['cosmic-files'] = 'com.system76.CosmicFiles' - gui_apps_to_launch['cosmic-player'] = 'com.system76.CosmicPlayer' - gui_apps_to_launch['cosmic-reader'] = 'com.system76.CosmicReader' - gui_apps_to_launch['cosmic-settings'] = 'com.system76.CosmicSettings' - gui_apps_to_launch['cosmic-store'] = 'com.system76.CosmicStore' - gui_apps_to_launch['cosmic-term'] = 'com.system76.CosmicTerm' - - for gui_app, app_id in gui_apps_to_launch.items(): - # Don't fail the test if binary is absent - if machine.execute(f"su - ${user.name} -c 'command -v {gui_app}'", timeout=5)[0] == 0: - match gui_app: - case 'cosmic-reader': - opt_arg = '${emptyPDF}/empty.pdf' - case _: - opt_arg = "" - - machine.succeed(f"su - ${user.name} -c 'WAYLAND_DISPLAY=wayland-1 XDG_RUNTIME_DIR=/run/user/${toString user.uid} ${DISPLAY} {gui_app} {opt_arg} >&2 &'", timeout=5) - # Nix builds the following non-commented expression to the following: - # `su - alice -c 'WAYLAND_DISPLAY=wayland-1 XDG_RUNTIME_DIR=/run/user/1000 lswt --json | jq ".toplevels" | grep "^ \\"app-id\\": \\"{app_id}\\"$"' ` - machine.wait_until_succeeds(f''''su - ${user.name} -c 'WAYLAND_DISPLAY=wayland-1 XDG_RUNTIME_DIR=/run/user/${toString user.uid} lswt --json | jq ".toplevels" | grep "^ \\"app-id\\": \\"{app_id}\\"$"' '''', timeout=60) - machine.succeed(f"pkill {gui_app}", timeout=5) - - machine.succeed("echo 'test completed succeessfully' > /${testName}", timeout=5) - machine.copy_from_machine('/${testName}') - - machine.shutdown() - ''; -} diff --git a/nixos/tests/cosmic/default.nix b/nixos/tests/cosmic/default.nix new file mode 100644 index 0000000000000..3b6dce9f55a42 --- /dev/null +++ b/nixos/tests/cosmic/default.nix @@ -0,0 +1,168 @@ +{ + config, + lib, + testName, + enableAutologin, + enableXWayland, + ... +}: + +let + user = config.nodes.machine.users.users.alice; + logFilePath = "/home/${user.name}/${testName}"; + # Use `writeShellScriptBin` instead of `writeShellScript` so that the + # process name in the journald log appears as 'cosmicTest[$pid]' + cosmicTest = config.node.pkgs.writeShellScriptBin "cosmicTest" '' + exec ${lib.getExe config.node.pkgs.python3Minimal} ${./test-script.py} \ + --log-file-path ${logFilePath} \ + --cosmic-reader-pdf ${config.node.pkgs.empty-pdf} \ + --polkit-agent-helper-path ${config.node.pkgs.polkit.out}/lib/polkit-1/polkit-agent-helper-1 \ + --root-user-password ${user.password} + ''; + cosmicTestDesktop = config.node.pkgs.makeDesktopItem { + name = "cosmicTest"; + desktopName = "COSMIC NixOS VM test (${testName})"; + exec = "cosmicTest"; + }; + cosmicTestAutostartItem = config.node.pkgs.makeAutostartItem { + name = "cosmicTest"; + package = cosmicTestDesktop; + }; +in + +{ + name = testName; + + meta.maintainers = lib.teams.cosmic.members; + + nodes.machine = { + imports = [ ../common/user-account.nix ]; + + services = { + # For `cosmic-store` to be added to `environment.systemPackages` + # and for it to work correctly because Flatpak is a runtime + # dependency of `cosmic-store`. + flatpak.enable = true; + + displayManager.cosmic-greeter.enable = true; + desktopManager.cosmic = { + enable = true; + xwayland.enable = enableXWayland; + }; + }; + + services.displayManager.autoLogin = lib.mkIf enableAutologin { + enable = true; + user = user.name; + }; + + users.users = { + alice.extraGroups = [ + "uinput" # for ydotoold + ]; + + root.password = user.password; + root.hashedPasswordFile = lib.mkForce null; + }; + + hardware.uinput.enable = true; + + environment.systemPackages = with config.node.pkgs; [ + ydotool + cosmicTest + cosmicTestAutostartItem + + # These two packages are used to check if a window was opened + # under the COSMIC session or not. Kinda important. + # TODO: Move the check from the test module to + # `nixos/lib/test-driver/src/test_driver/machine.py` so more + # Wayland-only testing can be done using the existing testing + # infrastructure. + jq + lswt + ]; + + # So far, all COSMIC tests launch a few GUI applications. In doing + # so, the default allocated memory to the guest of 1024M quickly + # poses a very high risk of an OOM-shutdown which is worse than an + # OOM-kill. Because now, the test failed, but not for a genuine + # reason, but an OOM-shutdown. That's an inconclusive failure + # which might possibly mask an actual failure. Not enabling + # systemd-oomd because we need said applications running for a + # few seconds. So instead, bump the allocated memory to the guest + # from 1024M to 4x; 4096M. + virtualisation.memorySize = 4096; + }; + + testScript = + { nodes, ... }: + '' + #testName: ${testName} + import sys + '' + + ( + if enableAutologin then + '' + with subtest("cosmic-greeter initialisation"): + machine.wait_for_unit("graphical.target", timeout=120) + '' + else + '' + from time import sleep + + machine.wait_for_unit("graphical.target", timeout=120) + machine.wait_until_succeeds("pgrep --uid ${config.nodes.machine.users.users.cosmic-greeter.name} --full cosmic-greeter", timeout=30) + # Sleep for 10 seconds for ensuring that `greetd` loads the + # password prompt for the login screen properly. + sleep(10) + + with subtest("cosmic-session login"): + machine.send_chars("${user.password}\n", delay=0.2) + '' + ) + + '' + with subtest("xdg autostart support in cosmic"): + # When checking the status of our `cosmicTest` package with: + # `machine.wait_for_unit("app-cosmicTest@autostart.service", user="${user.name}")` + # We are immediately greeted with the error: + # ``` + # subtest: xdg autostart support in cosmic + # machine: waiting for unit app-cosmicTest@autostart.service with user alice + # machine # [ 26.497516] cosmic-comp[1352]: [EGL] 0x3008 (BAD_DISPLAY) eglCreateSync: _eglCreateSync + # machine # [ 26.511706] su[1416]: Successful su for alice by root + # machine # [ 26.528190] su[1416]: pam_unix(su:session): session opened for user alice(uid=1000) by (uid=0) + # machine # Failed to connect to user scope bus via local transport: No such file or directory + # machine # [ 26.599563] su[1416]: pam_unix(su:session): session closed for user alice + # !!! Test "xdg autostart support in cosmic" failed with error: "retrieving systemctl property "ActiveState" for unit "app-cosmicTest@autostart.service" under user "alice" failed with exit code 1" + # ``` + # Meaning, our session is extremely new and the D-Bus user + # session socket does not yet exist. Instead, lets poll for + # the log file that the test is guaranteed to write to, as + # soon as it starts. + machine.wait_for_file("${logFilePath}.log", timeout=120) + + exit_code = 0 + try: + machine.wait_for_file("${logFilePath}.done", timeout=700) + except Exception: + exit_code = 1 + + # The log file is created in the very beginning of the test + # script's execution. If we are here, it means that the + # `wait_for_unit`'s "guard" on the test script's autostart unit + # plus the 630 second combined timeout of other two + # `wait_for_file`s, make it extremely likely for the log file to + # be present. + machine.copy_from_machine("${logFilePath}.log") + + machine.shutdown() + + with open(f"{machine.out_dir}/${testName}.log") as test_log_file: + contents = test_log_file.read() + print(contents) + if any("Z [ERROR] [L:" in line for line in contents.splitlines()): + exit_code = 1 + + sys.exit(exit_code) + ''; +} diff --git a/nixos/tests/cosmic/test-script.py b/nixos/tests/cosmic/test-script.py new file mode 100644 index 0000000000000..07377fa95facf --- /dev/null +++ b/nixos/tests/cosmic/test-script.py @@ -0,0 +1,315 @@ +#!/usr/bin/env python3 +import argparse +import logging +import os +import pathlib +import subprocess +import time + + +def parse_cli_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument( + "--log-file-path", + required=True, + type=str, + help="The path to the log file (without the '.log' suffix/extension)", + ) + parser.add_argument( + "--cosmic-reader-pdf", + required=True, + type=str, + help="The PDF that the `cosmic-reader` should open for testing", + ) + parser.add_argument( + "--polkit-agent-helper-path", + required=True, + type=str, + help="The path to the polkit agent helper (`${pkgs.polkit.out}/lib/polkit-1/polkit-agent-helper-1`)", + ) + parser.add_argument( + "--root-user-password", required=True, type=str, help="The root user's password" + ) + args = parser.parse_args() + return args + + +def start_ydotool_daemon() -> tuple[str, subprocess.Popen]: + """ + The ydotool requires a daemon to be running. + """ + xdg_runtime_dir = os.getenv("XDG_RUNTIME_DIR") or f"/run/user/{os.getuid()}" + ydotool_daemon_socket_path = f"{xdg_runtime_dir}/.ydotool_socket" + ydotool_daemon_process = subprocess.Popen( + [ + "ydotoold", + "--socket-path", + ydotool_daemon_socket_path, + "--mouse-off", + ], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + return ydotool_daemon_socket_path, ydotool_daemon_process + + +def wait_for_cosmic_de_readiness() -> None: + """ + Wait for the COSMIC DE to be ready, before running the tests. This + is done by waiting on the supposedly last component of the COSMIC + DE to be "ready." That component is the notification watcher, of + the `cosmic-applet` derivation. + """ + logging.info("=" * 80) + logging.info("Waiting for COSMIC DE to complete initialization") + + notification_watcher_wait_deadline = time.monotonic() + 360 + notification_watcher_exists = False + while time.monotonic() < notification_watcher_wait_deadline: + busctl_process = subprocess.run( + ["busctl", "--user", "status", "com.system76.CosmicStatusNotifierWatcher"], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + if busctl_process.returncode == 0: + notification_watcher_exists = True + break + else: + time.sleep(1) + logging_msg = "The COSMIC DE is " + if notification_watcher_exists: + logging.info(f"{logging_msg} ready") + else: + logging.error(f"{logging_msg} not ready") + return + + +def perform_polkit_authentication_test( + cli_args: argparse.Namespace, + ydotool_daemon_socket_path: str, + ydotool_daemon_process: subprocess.Popen, +) -> None: + """ + 1. Run `pkexec` as a background process that produces a specific + output to stdout upon successful completion. + 2. Wait unil it has been confimred that `cosmic-osd` has created + a pop-up requesting the root user's password. + 3. Use ydotool to type the root user's password in the pop-up + prompt. + 4. Ensure that the the `pkexec` background process' stdout matches + the output that we expect. + + Any breakage in this flow is considered a failure of the polkit + authenticaion test. + """ + logging.info("=" * 80) + logging.info("Performing polkit authentication test") + + polkit_test_passed = False + polkit_test_command = [ + "pkexec", + "--disable-internal-agent", + "bash", + "-c", + "echo -n 'polkit test was successful'", + ] + logging.info(f"Running: {polkit_test_command}") + polkit_test_process = subprocess.Popen( + polkit_test_command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + + polkit_popup_deadline = time.monotonic() + 60 + pop_up_msg = "the pop-up for polkit password authentication" + encountered_polkit_authentication_popup = False + while time.monotonic() < polkit_popup_deadline: + polkit_popup_check_process = subprocess.run( + [ + "pgrep", + "-afx", + f"{cli_args.polkit_agent_helper_path} --socket-activated", + ], + check=False, + ) + if polkit_popup_check_process.returncode == 0: + encountered_polkit_authentication_popup = True + logging.info(f"Noticed {pop_up_msg}") + if ydotool_daemon_process.poll() is None: + # The polkit-agent-helper process exists, but that + # doesn't necessarily mean that the pop-up is + # **rendered** and ready to accept the password. So we + # sleep for a few seconds. + time.sleep(20) + ydotool_process = subprocess.run( + [ + "ydotool", + "type", + "--key-delay=500", + f"{cli_args.root_user_password}\n", + ], + env={ + **os.environ.copy(), + "YDOTOOL_SOCKET": ydotool_daemon_socket_path, + }, + check=False, + ) + ydotool_msg = ( + "the root user's password in the pop-up for polkit authentication" + ) + if ydotool_process.returncode == 0: + logging.info(f"ydotool typed {ydotool_msg}") + else: + logging.error(f"ydotool did not type {ydotool_msg}") + else: + logging.error( + "The ydotool daemon exited for some reason before it could be used" + ) + break + time.sleep(1) + if not encountered_polkit_authentication_popup: + logging.error(f"Did not notice {pop_up_msg}") + + polkit_test_process_stdout = "" + polkit_test_process_stderr = "" + try: + polkit_test_process_stdout, polkit_test_process_stderr = ( + polkit_test_process.communicate(timeout=45) + ) + except subprocess.TimeoutExpired: + polkit_test_process.kill() + polkit_test_process_stdout, polkit_test_process_stderr = ( + polkit_test_process.communicate() + ) + + logging.info(f"polkit stdout: '{polkit_test_process_stdout}'") + logging.info(f"polkit stderr: '{polkit_test_process_stderr}'") + + if polkit_test_process_stdout: + logging.info(f"pkexec command stdout: {polkit_test_process_stdout}") + polkit_test_passed = "polkit test was successful" in polkit_test_process_stdout + else: + logging.warning("Could not capture stdout from the polkit test command") + + if polkit_test_passed: + logging.info("The polkit authentication test passed") + else: + logging.error("The polkit authentication test failed") + return + + +def perform_gui_application_test(cli_args: argparse.Namespace) -> None: + """ + 1. Start one GUI application as a background process. + 2. Wait unil it has been confimred that the GUI application is + running. + 3. Kill the background process of the GUI application. + + Any breakage in this flow is considered a failure of the test for + the GUI application. + """ + logging.info("=" * 80) + logging.info("Performing test to launch GUI applications") + + gui_apps_to_test = { + "com.system76.CosmicEdit": [ + "cosmic-edit", + ], + "com.system76.CosmicFiles": [ + "cosmic-files", + ], + "com.system76.CosmicPlayer": [ + "cosmic-player", + ], + "com.system76.CosmicReader": [ + "cosmic-reader", + cli_args.cosmic_reader_pdf, + ], + "com.system76.CosmicSettings": [ + "cosmic-settings", + ], + "com.system76.CosmicStore": [ + "cosmic-store", + ], + "com.system76.CosmicTerm": [ + "cosmic-term", + ], + } + + for gui_app_id, gui_app_command in gui_apps_to_test.items(): + logging.info(f"Running: {gui_app_command}") + gui_app_bg_process = subprocess.Popen( + gui_app_command, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + gui_app_bg_process_deadline = time.monotonic() + 30 + gui_app_is_running = False + + while time.monotonic() < gui_app_bg_process_deadline and not gui_app_is_running: + lswt_process = subprocess.run( + [ + "lswt", + "--custom", + "a", + ], + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) + lswt_process_stdout = lswt_process.stdout.strip() + if lswt_process_stdout: + if gui_app_id in lswt_process_stdout.splitlines(): + gui_app_is_running = True + time.sleep(1) + pkill_process = subprocess.run( + ["pkill", gui_app_command[0]], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + + log_message = ( + f"The GUI application test for '{gui_app_command[0]}' ({gui_app_id})" + ) + if gui_app_is_running: + logging.info(f"{log_message} passed") + else: + logging.error(f"{log_message} failed") + return + + +def main() -> None: + cli_args = parse_cli_args() + logging.basicConfig( + level=logging.INFO, + format=f"%(asctime)sZ [%(levelname)s] [L:%(lineno)d] %(message)s", + datefmt="%H:%M:%S", + handlers=[ + logging.StreamHandler(), + logging.FileHandler(f"{cli_args.log_file_path}.log", mode="w"), + ], + ) + logging.Formatter.converter = time.gmtime + logging.info(f"Logging to '{cli_args.log_file_path}.log'") + + ydotool_daemon_socket_path, ydotool_daemon_process = start_ydotool_daemon() + + # Wait for the DE to be ready + wait_for_cosmic_de_readiness() + + # tests go here + perform_polkit_authentication_test( + cli_args, ydotool_daemon_socket_path, ydotool_daemon_process + ) + perform_gui_application_test(cli_args) + + pathlib.Path(f"{cli_args.log_file_path}.done").touch() + return + + +if __name__ == "__main__": + main() diff --git a/nixos/tests/lomiri.nix b/nixos/tests/lomiri.nix index 5dc43286c0f3d..61893abe91a18 100644 --- a/nixos/tests/lomiri.nix +++ b/nixos/tests/lomiri.nix @@ -669,7 +669,7 @@ in # Doing this here, since we need an in-session shell & separately starting a terminal again wastes time with subtest("polkit agent works"): - machine.send_chars("pkexec touch /tmp/polkit-test\n") + machine.send_chars("run0 touch /tmp/polkit-test\n") # There's an authentication notification here that gains focus, but we struggle with OCRing it # Just hope that it's up after a short wait machine.sleep(10) diff --git a/nixos/tests/rtkit.nix b/nixos/tests/rtkit.nix index 74bf69e02a832..295fd36a13919 100644 --- a/nixos/tests/rtkit.nix +++ b/nixos/tests/rtkit.nix @@ -55,7 +55,7 @@ # Provide a little logging of polkit checks - otherwise it's # impossible to know what's going on. - security.polkit.debug = true; + security.polkit.extraArgs = [ "--log-level=notice" ]; security.polkit.extraConfig = '' polkit.addRule(function(action, subject) { const ns = "org.freedesktop.RealtimeKit1."; diff --git a/nixos/tests/timekpr.nix b/nixos/tests/timekpr.nix index 1ae793d8f70ed..9f832d2b62dde 100644 --- a/nixos/tests/timekpr.nix +++ b/nixos/tests/timekpr.nix @@ -1,13 +1,15 @@ -{ pkgs, lib, ... }: +{ + lib, + ... +}: + { name = "timekpr"; meta.maintainers = [ lib.maintainers.atry ]; - nodes.machine = - { pkgs, lib, ... }: - { - services.timekpr.enable = true; - }; + containers.machine = { + services.timekpr.enable = true; + }; testScript = '' start_all() diff --git a/pkgs/by-name/em/empty-pdf/package.nix b/pkgs/by-name/em/empty-pdf/package.nix new file mode 100644 index 0000000000000..55b13e0793552 --- /dev/null +++ b/pkgs/by-name/em/empty-pdf/package.nix @@ -0,0 +1,41 @@ +{ + stdenvNoCC, + imagemagick, + lib, +}: + +stdenvNoCC.mkDerivation { + name = "empty-pdf"; + + __structuredAttrs = true; + strictDeps = true; + + dontUnpack = true; + + nativeBuildInputs = [ imagemagick ]; + + buildPhase = '' + runHook preBuild + + magick xc:none -page Letter empty.pdf + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + mv empty.pdf $out + + runHook postInstall + ''; + + meta = { + description = "Empty PDF file intended for testing"; + maintainers = with lib.maintainers; [ + pandapip1 + thefossguy + ]; + platforms = imagemagick.meta.platforms; + }; +}