From b8a9aef153d2c4802158254d51575b8accc0b233 Mon Sep 17 00:00:00 2001 From: Manuel Bluhm Date: Sun, 15 Dec 2024 17:25:15 +0400 Subject: [PATCH] feat(user accounts): new ghaf user account setup - introducing userborn - disabling mutable users - re-factoring ghaf account to admin account - introducing login user account with homed + auxiliary accounts - impermanence flake input pinned to userborn patch - /etc/machine-id (gui-vm) is currently hardcoded as login user identity file depends on it. It should be generated on first boot and persistet. Workaround is available upstream (after userborn patch) in impermanence but does not seem to work with our setup, investigation required - reverts .face patch, works by copying .face to user home - known login incoveniences: wrong password entry on lock results in multiple errors, user needs to click login if fprint is enabled (only for login not lock) Signed-off-by: Manuel Bluhm --- flake.lock | 7 +- flake.nix | 2 +- modules/common/default.nix | 2 +- modules/common/profiles/debug.nix | 1 - modules/common/profiles/release.nix | 2 +- modules/common/services/audio.nix | 7 - modules/common/services/fprint.nix | 23 +- modules/common/services/xdgopener.nix | 4 +- modules/common/systemd/base.nix | 10 +- modules/common/users/accounts.nix | 66 ---- modules/common/users/admin.nix | 106 ++++++ modules/common/users/common.nix | 25 ++ modules/common/users/default.nix | 10 + modules/common/users/desktop.nix | 301 ++++++++++++++++++ modules/common/users/managed.nix | 161 ++++++++++ modules/desktop/graphics/labwc.config.nix | 2 +- modules/desktop/graphics/labwc.nix | 8 +- modules/givc/appvm.nix | 3 - modules/givc/audiovm.nix | 3 +- modules/givc/common.nix | 2 +- modules/givc/netvm.nix | 3 +- modules/hardware/common/devices.nix | 3 - modules/hardware/common/shared-mem.nix | 24 +- .../virtualization/microvm/adminvm.nix | 14 +- .../microvm/virtualization/microvm/appvm.nix | 22 +- .../virtualization/microvm/audiovm.nix | 72 ++--- .../microvm/common/ghaf-audio.nix | 2 +- .../microvm/common/shared-directory.nix | 12 +- .../microvm/common/storagevm.nix | 52 ++- .../microvm/virtualization/microvm/guivm.nix | 93 +++--- .../virtualization/microvm/idsvm/idsvm.nix | 1 - .../virtualization/microvm/microvm-host.nix | 67 +++- .../virtualization/microvm/modules.nix | 19 +- .../microvm/virtualization/microvm/netvm.nix | 73 ++--- modules/reference/personalize/accounts.nix | 44 +++ modules/reference/personalize/default.nix | 7 +- modules/reference/personalize/keys.nix | 2 +- packages/ghaf-powercontrol/default.nix | 2 +- packages/ghaf-xdg-open/default.nix | 12 +- packages/ssh-keys-helper/default.nix | 2 +- packages/wifi-signal-strength/default.nix | 65 ---- 41 files changed, 946 insertions(+), 390 deletions(-) delete mode 100644 modules/common/users/accounts.nix create mode 100644 modules/common/users/admin.nix create mode 100644 modules/common/users/common.nix create mode 100644 modules/common/users/default.nix create mode 100644 modules/common/users/desktop.nix create mode 100644 modules/common/users/managed.nix create mode 100644 modules/reference/personalize/accounts.nix delete mode 100644 packages/wifi-signal-strength/default.nix diff --git a/flake.lock b/flake.lock index 26330790a..15bb3e8ca 100644 --- a/flake.lock +++ b/flake.lock @@ -310,16 +310,17 @@ }, "impermanence": { "locked": { - "lastModified": 1731242966, - "narHash": "sha256-B3C3JLbGw0FtLSWCjBxU961gLNv+BOOBC6WvstKLYMw=", + "lastModified": 1728049659, + "narHash": "sha256-lGtad92Y/TnqpXRlZ1syiEq5czpvblKmcypeqGPiVF4=", "owner": "nix-community", "repo": "impermanence", - "rev": "3ed3f0eaae9fcc0a8331e77e9319c8a4abd8a71a", + "rev": "32b1094d28d5fbedcc85a403bc08c8877b396255", "type": "github" }, "original": { "owner": "nix-community", "repo": "impermanence", + "rev": "32b1094d28d5fbedcc85a403bc08c8877b396255", "type": "github" } }, diff --git a/flake.nix b/flake.nix index c30670bfa..952a5a684 100644 --- a/flake.nix +++ b/flake.nix @@ -140,7 +140,7 @@ }; impermanence = { - url = "github:nix-community/impermanence"; + url = "github:nix-community/impermanence/32b1094d28d5fbedcc85a403bc08c8877b396255"; }; givc = { diff --git a/modules/common/default.nix b/modules/common/default.nix index 26011fa03..6ce4a0b3a 100644 --- a/modules/common/default.nix +++ b/modules/common/default.nix @@ -11,7 +11,7 @@ ./firewall ./profiles ./security - ./users/accounts.nix + ./users ./version ./virtualization/docker.nix ./systemd diff --git a/modules/common/profiles/debug.nix b/modules/common/profiles/debug.nix index 21a5194f1..3b7020f64 100644 --- a/modules/common/profiles/debug.nix +++ b/modules/common/profiles/debug.nix @@ -15,7 +15,6 @@ in config = lib.mkIf cfg.enable { # Enable default accounts and passwords ghaf = { - users.accounts.enable = true; # Enable development on target development = { nix-setup.enable = true; diff --git a/modules/common/profiles/release.nix b/modules/common/profiles/release.nix index f06a6a72f..056007f54 100644 --- a/modules/common/profiles/release.nix +++ b/modules/common/profiles/release.nix @@ -18,6 +18,6 @@ in # TODO this needs to be refined when we define a policy for the # processes and the UID/groups that should be enabled by default # if not already covered by systemd - ghaf.users.accounts.enable = true; + # ghaf.users.admin.enable = true; }; } diff --git a/modules/common/services/audio.nix b/modules/common/services/audio.nix index 00d0e541a..ff4cd36c7 100644 --- a/modules/common/services/audio.nix +++ b/modules/common/services/audio.nix @@ -81,13 +81,6 @@ in }; }; - # Allow ghaf user to access pulseaudio and pipewire - users.extraUsers.ghaf.extraGroups = [ - "audio" - "video" - "pipewire" - ]; - # Start pipewire on system boot systemd.services.pipewire.wantedBy = [ "multi-user.target" ]; diff --git a/modules/common/services/fprint.nix b/modules/common/services/fprint.nix index 87654b43f..e13eabf05 100644 --- a/modules/common/services/fprint.nix +++ b/modules/common/services/fprint.nix @@ -46,38 +46,19 @@ in // Allow user to verify fingerprints polkit.addRule(function(action, subject) { if (action.id == "net.reactivated.fprint.device.verify" && - subject.user == "ghaf") { + subject.isInGroup ("users")) { return polkit.Result.YES; } }); // Allow user to enroll fingerprints polkit.addRule(function(action, subject) { if (action.id == "net.reactivated.fprint.device.enroll" && - subject.user == "ghaf") { + subject.isInGroup ("users")) { return polkit.Result.YES; } }); ''; }; - # PAM rules for swaylock fingerprint reader - pam.services = { - swaylock.text = '' - # Account management. - account required pam_unix.so - - # Authentication management. - auth sufficient pam_unix.so likeauth try_first_pass - auth sufficient ${pkgs.fprintd}/lib/security/pam_fprintd.so - auth required pam_deny.so - - # Password management. - password sufficient pam_unix.so nullok sha512 - - # Session management. - session required pam_env.so conffile=/etc/pam/environment readenv=0 - session required pam_unix.so - ''; - }; }; }; } diff --git a/modules/common/services/xdgopener.nix b/modules/common/services/xdgopener.nix index a7e7985d4..cc0b726b1 100644 --- a/modules/common/services/xdgopener.nix +++ b/modules/common/services/xdgopener.nix @@ -22,6 +22,7 @@ let # into all targets ghaf-xdg-open = pkgs.callPackage ../../../packages/ghaf-xdg-open { inherit (config.ghaf.security.sshKeys) sshKeyPath; + user = config.ghaf.users.appUser.name; }; in { @@ -51,9 +52,6 @@ in services."xdg@" = { description = "XDG opener"; serviceConfig = { - # The user 'ghaf' is used here to access SSH keys for the scp command - # This is required to copy files to the zathuravm - User = "ghaf"; ExecStart = "${ghaf-xdg-open}/bin/ghaf-xdg-open"; StandardInput = "socket"; StandardOutput = "journal"; diff --git a/modules/common/systemd/base.nix b/modules/common/systemd/base.nix index 786542ca4..4a6706923 100644 --- a/modules/common/systemd/base.nix +++ b/modules/common/systemd/base.nix @@ -30,10 +30,11 @@ let inherit (cfg) withAudit; withCompression = true; withCoredump = cfg.withDebug || cfg.withMachines; - inherit (cfg) withCryptsetup; + withCryptsetup = cfg.withCryptsetup || cfg.withHomed; inherit (cfg) withEfi; inherit (cfg) withBootloader; inherit (cfg) withFido2; + inherit (cfg) withHomed; inherit (cfg) withHostnamed; withImportd = cfg.withMachines; withKexectools = cfg.withDebug; @@ -55,6 +56,7 @@ let inherit (cfg) withTimesyncd; inherit (cfg) withTpm2Tss; inherit (cfg) withUkify; + withUserDb = cfg.withHomed; withUtmp = cfg.withJournal || cfg.withAudit; } // lib.optionalAttrs (lib.strings.versionAtLeast pkgs.systemdMinimal.version "255.0") { @@ -230,6 +232,12 @@ in default = false; }; + withHomed = mkOption { + description = "Enable systemd homed for users home functionality."; + type = types.bool; + default = false; + }; + withHostnamed = mkOption { description = "Enable systemd hostname daemon."; type = types.bool; diff --git a/modules/common/users/accounts.nix b/modules/common/users/accounts.nix deleted file mode 100644 index 01b24aa20..000000000 --- a/modules/common/users/accounts.nix +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright 2022-2024 TII (SSRC) and the Ghaf contributors -# SPDX-License-Identifier: Apache-2.0 -{ config, lib, ... }: -# account for the development time login with sudo rights -let - cfg = config.ghaf.users.accounts; - inherit (lib) - mkEnableOption - mkOption - optionals - mkIf - types - ; -in -{ - #TODO Extend this to allow definition of multiple users - options.ghaf.users.accounts = { - enable = mkEnableOption "Default account Setup"; - user = mkOption { - default = "ghaf"; - type = with types; str; - description = '' - A default user to create in the system. - ''; - }; - uid = mkOption { - default = 1000; - type = with types; int; - description = '' - A default user id for the user. - ''; - }; - password = mkOption { - default = "ghaf"; - type = with types; str; - description = '' - A default password for the user. - ''; - }; - }; - - config = mkIf cfg.enable { - users = { - mutableUsers = false; - users."${cfg.user}" = { - isNormalUser = true; - inherit (cfg) password; - inherit (cfg) uid; - #TODO add "docker" use "lib.optionals" - extraGroups = [ - "wheel" - "video" - "networkmanager" - ] ++ optionals config.security.tpm2.enable [ "tss" ]; - }; - groups."${cfg.user}" = { - name = cfg.user; - members = [ cfg.user ]; - }; - }; - - # to build ghaf as ghaf-user with caches - nix.settings.trusted-users = mkIf config.ghaf.profiles.debug.enable [ cfg.user ]; - #services.userborn.enable = true; - }; -} diff --git a/modules/common/users/admin.nix b/modules/common/users/admin.nix new file mode 100644 index 000000000..0e7b637d5 --- /dev/null +++ b/modules/common/users/admin.nix @@ -0,0 +1,106 @@ +# Copyright 2022-2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + config, + lib, + ... +}: +let + cfg = config.ghaf.users.admin; + inherit (lib) + mkIf + types + mkOption + optionals + ; +in +{ + options.ghaf.users.admin = { + enable = mkOption { + description = "Enable the admin user account. Enabled by default."; + type = types.bool; + default = true; + }; + name = mkOption { + description = "Admin account name. Defaults to 'ghaf'."; + type = types.str; + default = "ghaf"; + }; + uid = mkOption { + description = "User identifier (uid) for the admin account."; + type = types.int; + default = 1001; + }; + initialPassword = mkOption { + description = "Default password for the admin user account."; + type = types.nullOr types.str; + default = "ghaf"; + }; + initialHashedPassword = mkOption { + description = "Initial hashed password for the admin user account."; + type = types.nullOr types.str; + default = null; + }; + hashedPassword = mkOption { + description = "Hashed password for live updates."; + type = types.nullOr types.str; + default = null; + }; + extraGroups = mkOption { + description = "Extra groups for the admin user."; + type = types.listOf types.str; + default = [ ]; + }; + }; + + config = mkIf cfg.enable { + + # Assertions + assertions = [ + { + assertion = + (cfg.initialPassword != null) + || (cfg.initialHashedPassword != null) + || (cfg.hashedPassword != null); + message = '' + No password set for the admin account. Please set one of the following options: + - initialPassword + - initialHashedPassword + - hashedPassword + to allow admin login. + ''; + } + ]; + + users = { + users = { + "${cfg.name}" = { + isNormalUser = true; + inherit (cfg) initialPassword; + inherit (cfg) initialHashedPassword; + inherit (cfg) hashedPassword; + inherit (cfg) uid; + createHome = false; + home = "/var/empty"; + extraGroups = + [ + "wheel" + "video" + ] + ++ cfg.extraGroups + ++ optionals config.security.tpm2.enable [ "tss" ] + ++ optionals config.ghaf.virtualization.docker.daemon.enable [ "docker" ]; + }; + }; + groups = { + "${cfg.name}" = { + inherit (cfg) name; + members = [ cfg.name ]; + }; + }; + }; + + # to build ghaf as admin with caches + nix.settings.trusted-users = mkIf config.ghaf.profiles.debug.enable [ cfg.name ]; + }; +} diff --git a/modules/common/users/common.nix b/modules/common/users/common.nix new file mode 100644 index 000000000..3bf1e7bc1 --- /dev/null +++ b/modules/common/users/common.nix @@ -0,0 +1,25 @@ +# Copyright 2022-2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + config, + lib, + ... +}: +let + inherit (lib) mkDefault hasAttr; + hasStorageVm = (hasAttr "storagevm" config.ghaf) && config.ghaf.storagevm.enable; +in +{ + # Common ghaf user settings + config = { + + # Disable mutable users + users.mutableUsers = mkDefault false; + + # Enable userborn + services.userborn = { + enable = mkDefault true; + passwordFilesLocation = if hasStorageVm then "/var/lib/nixos" else "/etc"; + }; + }; +} diff --git a/modules/common/users/default.nix b/modules/common/users/default.nix new file mode 100644 index 000000000..2f324ebad --- /dev/null +++ b/modules/common/users/default.nix @@ -0,0 +1,10 @@ +# Copyright 2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + imports = [ + ./common.nix + ./admin.nix + ./desktop.nix + ./managed.nix + ]; +} diff --git a/modules/common/users/desktop.nix b/modules/common/users/desktop.nix new file mode 100644 index 000000000..993f04c9e --- /dev/null +++ b/modules/common/users/desktop.nix @@ -0,0 +1,301 @@ +# Copyright 2022-2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.ghaf.users; + inherit (lib) + mkIf + types + mkMerge + mkOption + mkEnableOption + optionalString + concatStringsSep + ; + + loginUserAccount = types.submodule { + options = { + enable = mkEnableOption "Enable desktop login user account."; + uid = mkOption { + description = "Login user identifier (uid). Defaults to 1000 for compatibility."; + type = types.int; + default = 1000; + }; + extraGroups = mkOption { + description = "Extra groups for the login user."; + type = types.listOf types.str; + default = [ ]; + }; + homeSize = mkOption { + description = '' + Size of the home directory for the login user in MB (integer). + The integer size is inherited from the microvm volume size parameter. + Defaults to 800 GB (800000 MB). + ''; + type = types.int; + default = 800000; + }; + }; + }; + + auxiliaryAccount = types.submodule { + options = { + enable = mkEnableOption "Enable auxiliary user account."; + name = mkOption { + description = "Auxiliary user's name."; + type = types.str; + }; + extraGroups = mkOption { + description = "Extra groups for the auxiliary user."; + type = types.listOf types.str; + default = [ ]; + }; + }; + }; + +in +{ + options.ghaf.users = { + # Main UI user + loginUser = mkOption { + description = "User account for desktop login."; + type = loginUserAccount; + default = { }; + }; + # Proxy user for dbus + proxyUser = mkOption { + description = "User account for dbus proxy functionality."; + type = auxiliaryAccount; + }; + # App user for running applications + appUser = mkOption { + description = "User account to run applications."; + type = auxiliaryAccount; + }; + }; + + config = mkMerge [ + { + assertions = [ + { + assertion = cfg.loginUser.enable -> config.ghaf.systemd.withHomed; + message = "You cannot enable login user without systemd-homed. Enable homed service in systemd module."; + } + { + assertion = cfg.loginUser.enable -> !cfg.proxyUser.enable; + message = "You cannot enable both login and proxy users at the same time."; + } + { + assertion = cfg.loginUser.enable -> !cfg.appUser.enable; + message = "You cannot enable both login and app users at the same time."; + } + { + assertion = cfg.proxyUser.enable -> !cfg.appUser.enable; + message = "You cannot enable both proxy and app users at the same time."; + } + ]; + + # Hardcode auxiliary user names + ghaf.users.appUser.name = "appuser"; + ghaf.users.proxyUser.name = "proxyuser"; + + users = { + users = mkMerge [ + (mkIf cfg.proxyUser.enable { + "${cfg.proxyUser.name}" = { + isNormalUser = true; + createHome = false; + inherit (cfg.loginUser) uid; + inherit (cfg.proxyUser) extraGroups; + }; + }) + (mkIf cfg.appUser.enable { + "${cfg.appUser.name}" = { + isNormalUser = true; + createHome = true; + inherit (cfg.loginUser) uid; + inherit (cfg.appUser) extraGroups; + linger = true; + }; + }) + ]; + groups = mkMerge [ + (mkIf cfg.proxyUser.enable { + "${cfg.proxyUser.name}" = { + inherit (cfg.proxyUser) name; + members = [ cfg.proxyUser.name ]; + }; + }) + (mkIf cfg.appUser.enable { + "${cfg.appUser.name}" = { + inherit (cfg.appUser) name; + members = [ cfg.appUser.name ]; + }; + }) + ]; + }; + } + + # Login user setup with homed + (mkIf cfg.loginUser.enable { + + # Enable homed service + services.homed.enable = true; + + # First boot login user setup service + systemd.services.setup-ghaf-user = + let + userSetupScript = pkgs.writeShellApplication { + name = "setup-ghaf-user"; + runtimeInputs = [ + pkgs.coreutils + pkgs.ncurses + pkgs.brightnessctl + ]; + text = '' + brightnessctl set 100% + clear + echo -e "\e[1;32;1mWelcome to Ghaf \e[0m" + echo "" + echo "Start by creating your user account." + echo "" + + # Read new user name + ACCEPTABLE_USER=false + until $ACCEPTABLE_USER; do + echo -n "Enter your user name: " + read -e -r USERNAME + USERNAME=''${USERNAME// /_} + USERNAME=''${USERNAME//[^a-zA-Z0-9_]/} + USERNAME=''$(echo -n "$USERNAME" | tr '[:upper:]' '[:lower:]') + if grep -q -w "$USERNAME:" /etc/passwd; then + echo "User $USERNAME already exists. Please choose another user name." + else + ACCEPTABLE_USER=true + fi + done + + echo "" + echo -n "Enter your full name: " + read -e -r REALNAME + REALNAME=''${REALNAME//[^a-zA-Z ]/} + [[ -n "$REALNAME" ]] || REALNAME="$USERNAME"; + + echo "" + echo "Setting up your user account and creating encrypted home folder after you enter your password." + echo "This may take a while..." + echo "" + + # Add login user and home + homectl create "$USERNAME" \ + --real-name="$REALNAME" \ + --skel=/etc/skel \ + --storage=luks \ + --luks-pbkdf-type=argon2id \ + --enforce-password-policy=true \ + --drop-caches=true \ + --nosuid=true \ + --noexec=true \ + --nodev=true \ + --disk-size=${toString cfg.loginUser.homeSize}M \ + --shell=/run/current-system/sw/bin/bash \ + --uid=${toString cfg.loginUser.uid} \ + --member-of=users${ + optionalString ( + cfg.loginUser.extraGroups != [ ] + ) ",${concatStringsSep "," cfg.loginUser.extraGroups}" + } + + # Lock user creation script + install -m 000 /dev/null /var/lib/nixos/user.lock + + echo "" + echo "User $USERNAME created. Starting user session..." + sleep 1 + ''; + }; + in + { + description = "First boot user setup"; + enable = true; + requiredBy = [ "multi-user.target" ]; + before = [ "greetd.service" ]; + path = [ userSetupScript ]; + unitConfig.ConditionPathExists = "!/var/lib/nixos/user.lock"; + serviceConfig = { + Type = "oneshot"; + StandardInput = "tty"; + StandardOutput = "tty"; + StandardError = "journal"; + TTYPath = "/dev/tty1"; + TTYReset = true; + TTYVHangup = true; + ExecStart = "${userSetupScript}/bin/setup-ghaf-user"; + }; + }; + + systemd.services.setup-test-user = + let + automatedUserSetupScript = pkgs.writeShellApplication { + name = "setup-test-user"; + runtimeInputs = [ + pkgs.coreutils + ]; + text = '' + echo "Automated boot user setup script" + + # Hardcoded user name + USERNAME="testuser" + REALNAME="Test User" + export PASSWORD="testpw" + export NEWPASSWORD="testpw" + + # Add login user and home + homectl create "$USERNAME" \ + --real-name="$REALNAME" \ + --skel=/etc/skel \ + --storage=luks \ + --luks-pbkdf-type=argon2id \ + --enforce-password-policy=true \ + --drop-caches=true \ + --nosuid=true \ + --noexec=true \ + --nodev=true \ + --disk-size=${toString cfg.loginUser.homeSize}M \ + --shell=/run/current-system/sw/bin/bash \ + --uid=${toString cfg.loginUser.uid} \ + --member-of=users${ + optionalString ( + cfg.loginUser.extraGroups != [ ] + ) ",${concatStringsSep "," cfg.loginUser.extraGroups}" + } + + # Lock user creation script + install -m 000 /dev/null /var/lib/nixos/user.lock + echo "User $USERNAME created." + + # Stop interactive user setup service + systemctl stop setup-ghaf-user + ''; + }; + in + mkIf config.ghaf.profiles.debug.enable { + description = "Automated boot user setup script"; + enable = true; + path = [ automatedUserSetupScript ]; + unitConfig.ConditionPathExists = "!/var/lib/nixos/user.lock"; + serviceConfig = { + Type = "oneshot"; + StandardOutput = "journal"; + StandardError = "journal"; + ExecStart = "${automatedUserSetupScript}/bin/setup-test-user"; + }; + }; + }) + ]; +} diff --git a/modules/common/users/managed.nix b/modules/common/users/managed.nix new file mode 100644 index 000000000..3b40b5e1e --- /dev/null +++ b/modules/common/users/managed.nix @@ -0,0 +1,161 @@ +# Copyright 2022-2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + config, + lib, + ... +}: +let + cfg = config.ghaf.users; + inherit (lib) + mkIf + types + mkOption + optionals + optionalAttrs + ; + inherit (lib.attrsets) nameValuePair; + inherit (builtins) listToAttrs; + + userAccount = types.submodule { + options = { + name = mkOption { + description = "User name"; + type = types.nullOr types.str; + default = null; + }; + vms = mkOption { + description = "List of VMs (or host) the user is enabled in."; + type = types.listOf types.str; + default = [ ]; + }; + initialPassword = mkOption { + description = "Initial password for the admin user account."; + type = types.nullOr types.str; + default = null; + }; + initialHashedPassword = mkOption { + description = "Initial hashed password for the admin user account."; + type = types.nullOr types.str; + default = null; + }; + hashedPassword = mkOption { + description = "Hashed password for live updates."; + type = types.nullOr types.str; + default = null; + }; + uid = mkOption { + description = "Optional user identifier (uid). Defaults to null."; + type = types.nullOr types.int; + default = null; + }; + gid = mkOption { + description = "Optional primary group identifier (gid). Defaults to null."; + type = types.nullOr types.int; + default = null; + }; + createHome = mkOption { + description = "Create home directory for the user."; + type = types.bool; + default = true; + }; + linger = mkOption { + description = "Enable lingering for the user."; + type = types.bool; + default = false; + }; + extraGroups = mkOption { + description = "Extra groups for the user."; + type = types.listOf types.str; + default = [ ]; + }; + }; + }; +in +{ + options.ghaf.users = { + managed = mkOption { + description = '' + List of declarativively managed user accounts. + + The ghaf user interface for declarative users has the following options: + - No enable flag, a specified account is enabled by default + [mandatory] + - name: User name + - vms: List of VMs (or host) the user is enabled in + [optional] + - initialPassword: Default password for the user account + - initialHashedPassword: Initial hashed password for the user account + - hashedPassword: Hashed password for live updates + - uid: Optional user identifier (uid). Defaults to null + - gid: Optional primary group identifier (gid). Defaults to null + - createHome: Create home directory for the user + - linger: Enable lingering for the user + - extraGroups: Extra groups for the user + + These, as any additional user option, may be set through the usual NixOS user options. + ''; + type = types.listOf userAccount; + default = [ ]; + }; + }; + + config = + let + # Filter out applicable accounts for current system + accounts = lib.filter (acc: (lib.lists.any (name: name == config.system.name) acc.vms)) cfg.managed; + hasAccounts = accounts != [ ]; + in + mkIf hasAccounts { + + assertions = [ + { + assertion = + (config.system.name == "gui-vm") + -> (lib.lists.all ( + acc: + ( + acc.uid != null + && acc.uid != config.ghaf.users.loginUser.uid + && acc.uid != config.ghaf.users.admin.uid + ) + ) accounts); + message = "Users in the GUI VM must have a non-reserved uid specified."; + } + ]; + + users = { + users = listToAttrs ( + map ( + acc: + nameValuePair acc.name { + isNormalUser = true; + inherit (acc) initialPassword; + inherit (acc) initialHashedPassword; + inherit (acc) hashedPassword; + inherit (acc) createHome; + inherit (acc) linger; + inherit (acc) extraGroups; + } + // lib.optionalAttrs (acc.uid != null) { + inherit (acc) uid; + } + // lib.optionalAttrs (acc.gid != null) { + inherit (acc) gid; + } + ) accounts + ); + groups = listToAttrs ( + map ( + acc: + optionals (acc.gid == null) ( + nameValuePair acc.name { + inherit (acc) name; + members = [ acc.name ]; + } + ) + ) accounts + ); + }; + }; +} diff --git a/modules/desktop/graphics/labwc.config.nix b/modules/desktop/graphics/labwc.config.nix index 5848ba02c..85151cec6 100644 --- a/modules/desktop/graphics/labwc.config.nix +++ b/modules/desktop/graphics/labwc.config.nix @@ -342,7 +342,7 @@ in services.greetd.settings = { initial_session = lib.mkIf (cfg.autologinUser != null) { - user = "ghaf"; + user = config.ghaf.users.admin.name; command = "ghaf-session"; }; }; diff --git a/modules/desktop/graphics/labwc.nix b/modules/desktop/graphics/labwc.nix index 3e52afa10..c228686be 100644 --- a/modules/desktop/graphics/labwc.nix +++ b/modules/desktop/graphics/labwc.nix @@ -8,7 +8,6 @@ }: let cfg = config.ghaf.graphics.labwc; - userName = config.ghaf.users.accounts.user; in { options.ghaf.graphics.labwc = { @@ -27,7 +26,7 @@ in }; autologinUser = lib.mkOption { type = lib.types.nullOr lib.types.str; - default = userName; + default = config.ghaf.users.admin.name; description = '' Username of the account that will be automatically logged in to the desktop. If unspecified, the login manager is shown as usual. @@ -191,11 +190,6 @@ in # DBus service for accessing the list of user accounts and information attached to those accounts services.accounts-daemon.enable = true; - # We can explicitly specify the icon path, using which user can set custom image when system is locked - system.activationScripts.userIcon.text = '' - mkdir -p /var/lib/AccountsService/users - echo -e "[User]\nIcon=/home/${userName}/Pictures/.face\n" > /var/lib/AccountsService/users/${userName} - ''; ghaf.graphics.launchers = lib.mkIf config.ghaf.profiles.debug.enable [ { diff --git a/modules/givc/appvm.nix b/modules/givc/appvm.nix index f0e698e27..cb94b626d 100644 --- a/modules/givc/appvm.nix +++ b/modules/givc/appvm.nix @@ -46,8 +46,5 @@ in tls.enable = config.ghaf.givc.enableTls; admin = config.ghaf.givc.adminConfig; }; - - # Quick fix to allow linger (linger option in user def. currently doesn't work, e.g., bc mutable) - systemd.tmpfiles.rules = [ "f /var/lib/systemd/linger/${config.ghaf.users.accounts.user}" ]; }; } diff --git a/modules/givc/audiovm.nix b/modules/givc/audiovm.nix index a048f5c26..9accd06bc 100644 --- a/modules/givc/audiovm.nix +++ b/modules/givc/audiovm.nix @@ -42,8 +42,7 @@ in enable = true; system = { enable = true; - # TODO Change this with new user setup - user = "ghaf"; + user = config.ghaf.users.proxyUser.name; socket = "/tmp/dbusproxy_snd.sock"; policy = { talk = [ diff --git a/modules/givc/common.nix b/modules/givc/common.nix index a7b929f46..0b8e36f24 100644 --- a/modules/givc/common.nix +++ b/modules/givc/common.nix @@ -12,7 +12,7 @@ let mitmEnabled = config.ghaf.virtualization.microvm.idsvm.enable && config.ghaf.virtualization.microvm.idsvm.mitmproxy.enable; - mitmExtraArgs = lib.optionalString mitmEnabled "--user-data-dir=/home/${config.ghaf.users.accounts.user}/.config/google-chrome/Default --test-type --ignore-certificate-errors-spki-list=Bq49YmAq1CG6FuBzp8nsyRXumW7Dmkp7QQ/F82azxGU="; + mitmExtraArgs = lib.optionalString mitmEnabled "--user-data-dir=/home/${config.ghaf.users.appUser.name}/.config/google-chrome/Default --test-type --ignore-certificate-errors-spki-list=Bq49YmAq1CG6FuBzp8nsyRXumW7Dmkp7QQ/F82azxGU="; in { options.ghaf.givc = { diff --git a/modules/givc/netvm.nix b/modules/givc/netvm.nix index a1a7cf1d1..b760e76ec 100644 --- a/modules/givc/netvm.nix +++ b/modules/givc/netvm.nix @@ -50,8 +50,7 @@ in enable = true; system = { enable = true; - # TODO Change this with new user setup - user = "ghaf"; + user = config.ghaf.users.proxyUser.name; socket = "/tmp/dbusproxy_net.sock"; policy = { own = [ diff --git a/modules/hardware/common/devices.nix b/modules/hardware/common/devices.nix index 6e47345f8..09049009f 100644 --- a/modules/hardware/common/devices.nix +++ b/modules/hardware/common/devices.nix @@ -81,9 +81,6 @@ in ++ config.ghaf.hardware.definition.input.touchpad.evdev ++ config.ghaf.hardware.definition.input.misc.evdev ); - - # TODO: Remove this once wifi-signal-strength is changed - ghaf.hardware.definition.network.pciDevices = config.ghaf.hardware.definition.network.pciDevices; }; }; }; diff --git a/modules/hardware/common/shared-mem.nix b/modules/hardware/common/shared-mem.nix index 222894975..c8c7520dd 100644 --- a/modules/hardware/common/shared-mem.nix +++ b/modules/hardware/common/shared-mem.nix @@ -34,7 +34,7 @@ in type = types.int; default = 16; description = mdDoc '' - Specifies the size of the shared memory region, measured in + Specifies the size of the shared memory region, measured in megabytes (MB) ''; }; @@ -42,7 +42,7 @@ in type = types.str; default = "2M"; description = mdDoc '' - Specifies the size of the large memory page area. Supported kernel + Specifies the size of the large memory page area. Supported kernel values are 2 MB and 1 GB ''; apply = @@ -56,7 +56,7 @@ in type = types.path; default = "/tmp/ivshmem_socket"; # The value is hardcoded in the application description = mdDoc '' - Specifies the path to the shared memory socket, used by QEMU + Specifies the path to the shared memory socket, used by QEMU instances for inter-VM memory sharing and interrupt signaling ''; }; @@ -65,7 +65,7 @@ in default = "0x920000000"; description = mdDoc '' Maps the shared memory to a physical address if set to a non-zero value. - The address must be platform-specific and arbitrarily chosen to avoid + The address must be platform-specific and arbitrarily chosen to avoid conflicts with other memory areas, such as PCI regions. ''; }; @@ -93,19 +93,19 @@ in }; serverSocketPath = mkOption { type = types.path; - default = "/run/user/${builtins.toString config.ghaf.users.accounts.uid}/memsocket-server.sock"; + default = "/run/user/${builtins.toString config.ghaf.users.loginUser.uid}/memsocket-server.sock"; description = mdDoc '' - Specifies the path of the listening socket, which is used by Waypipe - or other server applications as the output socket in server mode for + Specifies the path of the listening socket, which is used by Waypipe + or other server applications as the output socket in server mode for data transmission ''; }; clientSocketPath = mkOption { type = types.path; - default = "/run/user/${builtins.toString config.ghaf.users.accounts.uid}/memsocket-client.sock"; + default = "/run/user/${builtins.toString config.ghaf.users.loginUser.uid}/memsocket-client.sock"; description = mdDoc '' - Specifies the location of the output socket, which will connected to - in order to receive data from AppVMs. This socket must be created by + Specifies the location of the output socket, which will connected to + in order to receive data from AppVMs. This socket must be created by another application, such as Waypipe, when operating in client mode ''; }; @@ -113,8 +113,8 @@ in type = types.bool; default = false; description = mdDoc '' - Enables the use of shared memory with Waypipe for Wayland-enabled - applications running on virtual machines (VMs), facilitating + Enables the use of shared memory with Waypipe for Wayland-enabled + applications running on virtual machines (VMs), facilitating efficient inter-VM communication ''; }; diff --git a/modules/microvm/virtualization/microvm/adminvm.nix b/modules/microvm/virtualization/microvm/adminvm.nix index 9e235a9ca..8fdee3b58 100644 --- a/modules/microvm/virtualization/microvm/adminvm.nix +++ b/modules/microvm/virtualization/microvm/adminvm.nix @@ -21,6 +21,7 @@ let ; internalIP = 10; }) + ./common/storagevm.nix # We need to retrieve mac address and start log aggregator ../../../common/logging/hw-mac-retrieve.nix ../../../common/logging/logs-aggregator.nix @@ -29,7 +30,7 @@ let { lib, ... }: { ghaf = { - users.accounts.enable = lib.mkDefault configHost.ghaf.users.accounts.enable; + # Profiles profiles.debug.enable = lib.mkDefault configHost.ghaf.profiles.debug.enable; development = { # NOTE: SSH port also becomes accessible on the network interface @@ -38,6 +39,8 @@ let debug.tools.enable = lib.mkDefault configHost.ghaf.development.debug.tools.enable; nix-setup.enable = lib.mkDefault configHost.ghaf.development.nix-setup.enable; }; + + # System systemd = { enable = true; withName = "adminvm-systemd"; @@ -49,18 +52,19 @@ let withDebug = configHost.ghaf.profiles.debug.enable; withHardenedConfigs = true; }; + givc.adminvm.enable = true; + + # Storage storagevm = { enable = true; - name = "adminvm"; + name = vmName; files = [ "/etc/locale-givc.conf" "/etc/timezone.conf" ]; }; - givc.adminvm.enable = true; - - # Log aggregation configuration + # Services logging = { client.enable = isLoggingEnabled; listener = { diff --git a/modules/microvm/virtualization/microvm/appvm.nix b/modules/microvm/virtualization/microvm/appvm.nix index 6e912a174..02872ce59 100644 --- a/modules/microvm/virtualization/microvm/appvm.nix +++ b/modules/microvm/virtualization/microvm/appvm.nix @@ -77,14 +77,24 @@ let }: { ghaf = { - users.accounts.enable = lib.mkDefault configHost.ghaf.users.accounts.enable; - profiles.debug.enable = lib.mkDefault configHost.ghaf.profiles.debug.enable; + # Profiles + users.appUser = { + enable = true; + extraGroups = [ + "audio" + "video" + "users" + ]; + }; + profiles.debug.enable = lib.mkDefault configHost.ghaf.profiles.debug.enable; development = { ssh.daemon.enable = lib.mkDefault configHost.ghaf.development.ssh.daemon.enable; debug.tools.enable = lib.mkDefault configHost.ghaf.development.debug.tools.enable; nix-setup.enable = lib.mkDefault configHost.ghaf.development.nix-setup.enable; }; + + # Systemd systemd = { enable = true; withName = "appvm-systemd"; @@ -106,8 +116,8 @@ let storagevm = { enable = true; - name = "${vm.name}"; - users.${config.ghaf.users.accounts.user}.directories = [ + name = vmName; + users.${config.ghaf.users.appUser.name}.directories = [ ".config/" "Downloads" "Music" @@ -130,7 +140,9 @@ let # setting mode), instead of symlinking it. environment.etc.${configHost.ghaf.security.sshKeys.getAuthKeysFilePathInEtc} = sshKeysHelper.getAuthKeysSource; - services.openssh = configHost.ghaf.security.sshKeys.sshAuthorizedKeysCommand; + services.openssh = configHost.ghaf.security.sshKeys.sshAuthorizedKeysCommand // { + authorizedKeysCommandUser = config.ghaf.users.appUser.name; + }; system.stateVersion = lib.trivial.release; diff --git a/modules/microvm/virtualization/microvm/audiovm.nix b/modules/microvm/virtualization/microvm/audiovm.nix index 8918fd6b6..cefb803df 100644 --- a/modules/microvm/virtualization/microvm/audiovm.nix +++ b/modules/microvm/virtualization/microvm/audiovm.nix @@ -4,21 +4,14 @@ { config, lib, - pkgs, ... }: let configHost = config; vmName = "audio-vm"; macAddress = "02:00:00:03:03:03"; - isGuiVmEnabled = config.ghaf.virtualization.microvm.guivm.enable; has_acpi_path = config.ghaf.hardware.definition.audio.acpiPath != null; - sshKeysHelper = pkgs.callPackage ../../../../packages/ssh-keys-helper { - inherit pkgs; - inherit config; - }; - audiovmBaseConfiguration = { imports = [ inputs.self.nixosModules.givc-audiovm @@ -40,14 +33,23 @@ let imports = [ ../../../common ]; ghaf = { - users.accounts.enable = lib.mkDefault configHost.ghaf.users.accounts.enable; + # Profiles profiles.debug.enable = lib.mkDefault configHost.ghaf.profiles.debug.enable; - development = { ssh.daemon.enable = lib.mkDefault configHost.ghaf.development.ssh.daemon.enable; debug.tools.enable = lib.mkDefault configHost.ghaf.development.debug.tools.enable; nix-setup.enable = lib.mkDefault configHost.ghaf.development.nix-setup.enable; }; + users.proxyUser = { + enable = true; + extraGroups = [ + "audio" + "video" + "pipewire" + ]; + }; + + # System systemd = { enable = true; withName = "audiovm-systemd"; @@ -61,14 +63,18 @@ let withHardenedConfigs = true; }; givc.audiovm.enable = true; + + # Storage + storagevm = { + enable = true; + name = vmName; + }; + + # Services services.audio.enable = true; # Logging client configuration logging.client.enable = configHost.ghaf.logging.client.enable; logging.client.endpoint = configHost.ghaf.logging.client.endpoint; - storagevm = { - enable = true; - name = "audiovm"; - }; }; environment = { @@ -87,32 +93,20 @@ let hostPlatform.system = configHost.nixpkgs.hostPlatform.system; }; - services.openssh = config.ghaf.security.sshKeys.sshAuthorizedKeysCommand; - microvm = { # Optimize is disabled because when it is enabled, qemu is built without libusb optimize.enable = false; vcpu = 2; mem = 384; hypervisor = "qemu"; - shares = - [ - { - tag = "ro-store"; - source = "/nix/store"; - mountPoint = "/nix/.ro-store"; - proto = "virtiofs"; - } - ] - ++ lib.optionals isGuiVmEnabled [ - { - # Add the waypipe-ssh public key to the microvm - tag = config.ghaf.security.sshKeys.waypipeSshPublicKeyName; - source = config.ghaf.security.sshKeys.waypipeSshPublicKeyDir; - mountPoint = config.ghaf.security.sshKeys.waypipeSshPublicKeyDir; - proto = "virtiofs"; - } - ]; + shares = [ + { + tag = "ro-store"; + source = "/nix/store"; + mountPoint = "/nix/.ro-store"; + proto = "virtiofs"; + } + ]; writableStoreOverlay = lib.mkIf config.ghaf.development.debug.tools.enable "/nix/.rw-store"; qemu = { machine = @@ -133,18 +127,6 @@ let ]; }; }; - - fileSystems = lib.mkIf isGuiVmEnabled { - ${config.ghaf.security.sshKeys.waypipeSshPublicKeyDir}.options = [ "ro" ]; - }; - - # SSH is very picky about to file permissions and ownership and will - # accept neither direct path inside /nix/store or symlink that points - # there. Therefore we copy the file to /etc/ssh/get-auth-keys (by - # setting mode), instead of symlinking it. - environment.etc = lib.mkIf isGuiVmEnabled { - ${config.ghaf.security.sshKeys.getAuthKeysFilePathInEtc} = sshKeysHelper.getAuthKeysSource; - }; } ) ]; diff --git a/modules/microvm/virtualization/microvm/common/ghaf-audio.nix b/modules/microvm/virtualization/microvm/common/ghaf-audio.nix index 58dc1b538..648f6f2ac 100644 --- a/modules/microvm/virtualization/microvm/common/ghaf-audio.nix +++ b/modules/microvm/virtualization/microvm/common/ghaf-audio.nix @@ -29,7 +29,7 @@ in config = lib.mkIf cfg.enable { security.rtkit.enable = cfg.useTunneling; - users.extraUsers.ghaf.extraGroups = lib.mkIf cfg.useTunneling [ + ghaf.users.appUser.extraGroups = lib.mkIf cfg.useTunneling [ "audio" "video" ]; diff --git a/modules/microvm/virtualization/microvm/common/shared-directory.nix b/modules/microvm/virtualization/microvm/common/shared-directory.nix index 80db4d668..bb995d549 100644 --- a/modules/microvm/virtualization/microvm/common/shared-directory.nix +++ b/modules/microvm/virtualization/microvm/common/shared-directory.nix @@ -4,10 +4,9 @@ name: { lib, config, ... }: let cfg = config.ghaf.storagevm; - shared-mountPath = "/tmp/shared/shares"; - inherit (config.ghaf.users.accounts) user; isGuiVm = builtins.stringLength name == 0; - userDir = "/home/${user}" + (if isGuiVm then "/Shares" else "/Unsafe\ share"); + shared-mountPath = "/tmp/shared/shares"; + userDir = if isGuiVm then "/Shares" else "/home/${config.ghaf.users.appUser.name}/Unsafe\ share"; in { config = lib.mkIf cfg.enable { @@ -37,5 +36,12 @@ in "x-gvfs-hide" ]; }; + + # Add bookmark to skel + environment.etc = lib.mkIf config.ghaf.users.loginUser.enable { + "skel/.gtk-bookmarks".text = '' + file:///Shares Shares + ''; + }; }; } diff --git a/modules/microvm/virtualization/microvm/common/storagevm.nix b/modules/microvm/virtualization/microvm/common/storagevm.nix index 15e3b84e0..d39698156 100644 --- a/modules/microvm/virtualization/microvm/common/storagevm.nix +++ b/modules/microvm/virtualization/microvm/common/storagevm.nix @@ -1,12 +1,22 @@ # Copyright 2022-2024 TII (SSRC) and the Ghaf contributors # SPDX-License-Identifier: Apache-2.0 -{ lib, config, ... }: +{ + lib, + config, + ... +}: let cfg = config.ghaf.storagevm; - mountPath = "/guestStorage"; + inherit (lib) + mkEnableOption + mkOption + mkIf + mkMerge + types + ; in { - options.ghaf.storagevm = with lib; { + options.ghaf.storagevm = { enable = mkEnableOption "StorageVM support"; name = mkOption { @@ -16,6 +26,14 @@ in type = types.str; }; + mountPath = mkOption { + description = '' + Mount path for the storage virtual machine. + ''; + type = types.str; + default = "/guestStorage"; + }; + directories = mkOption { # FIXME: Probably will lead to disgraceful error messages, as we # put typechecking on nix impermanence option. But other, @@ -61,7 +79,7 @@ in }; config = lib.mkIf cfg.enable { - fileSystems.${mountPath} = { + fileSystems.${cfg.mountPath} = { neededForBoot = true; options = [ "rw" @@ -70,7 +88,7 @@ in "noexec" ]; }; - virtualisation.fileSystems.${mountPath}.device = "/dev/vda"; + virtualisation.fileSystems.${cfg.mountPath}.device = "/dev/vda"; microvm.shares = [ { @@ -78,23 +96,41 @@ in proto = "virtiofs"; securityModel = "passthrough"; source = "/storagevm/${cfg.name}"; - mountPoint = mountPath; + mountPoint = cfg.mountPath; } ]; - environment.persistence.${mountPath} = lib.mkMerge [ + microvm.volumes = lib.optionals config.ghaf.users.loginUser.enable [ + { + image = "/storagevm/homes/${cfg.name}-home.img"; + size = builtins.floor (config.ghaf.users.loginUser.homeSize * 1.15); + fsType = "btrfs"; + mountPoint = "/home"; + } + ]; + + environment.persistence.${cfg.mountPath} = mkMerge [ { hideMounts = true; directories = [ "/var/lib/nixos" ]; - files = [ "/etc/ssh/ssh_host_ed25519_key.pub" "/etc/ssh/ssh_host_ed25519_key" ]; } { inherit (cfg) directories users files; } + (mkIf config.ghaf.users.loginUser.enable { + directories = [ + "/var/lib/systemd/home" + ]; + }) ]; + + # Workaround, fixes homed machine-id dependency + environment.etc = lib.optionalAttrs config.ghaf.users.loginUser.enable { + machine-id.text = "d8dee68f8d334c79ac8f8229921e0b25"; + }; }; } diff --git a/modules/microvm/virtualization/microvm/guivm.nix b/modules/microvm/virtualization/microvm/guivm.nix index 9dd1759d5..b6b9f3f26 100644 --- a/modules/microvm/virtualization/microvm/guivm.nix +++ b/modules/microvm/virtualization/microvm/guivm.nix @@ -66,24 +66,20 @@ let in { ghaf = { - users.accounts.enable = lib.mkDefault config.ghaf.users.accounts.enable; + # Profiles profiles = { debug.enable = lib.mkDefault config.ghaf.profiles.debug.enable; applications.enable = false; graphics.enable = true; }; - - # Create launchers for regular apps running in the GUIVM and virtualized ones if GIVC is enabled - graphics.launchers = guivmLaunchers ++ lib.optionals config.ghaf.givc.enable virtualLaunchers; - - # To enable screen locking set to true - graphics.labwc = { - autolock.enable = lib.mkDefault config.ghaf.graphics.labwc.autolock.enable; - autologinUser = lib.mkDefault config.ghaf.graphics.labwc.autologinUser; - securityContext = map (vm: { - identifier = vm.name; - color = vm.borderColor; - }) config.ghaf.virtualization.microvm.appvm.vms; + users = { + loginUser = { + enable = true; + extraGroups = [ + "audio" + "video" + ]; + }; }; development = { @@ -91,40 +87,50 @@ let debug.tools.enable = lib.mkDefault config.ghaf.development.debug.tools.enable; nix-setup.enable = lib.mkDefault config.ghaf.development.nix-setup.enable; }; + + # System systemd = { enable = true; withName = "guivm-systemd"; withAudit = config.ghaf.profiles.debug.enable; + withHomed = true; withLocaled = true; withNss = true; withResolved = true; withTimesyncd = true; withDebug = config.ghaf.profiles.debug.enable; - withHardenedConfigs = true; + withHardenedConfigs = false; }; givc.guivm.enable = true; - # Logging client configuration - logging.client.enable = config.ghaf.logging.client.enable; - logging.client.endpoint = config.ghaf.logging.client.endpoint; + + # Storage storagevm = { enable = true; - name = "guivm"; + name = vmName; directories = [ { directory = "/var/lib/private/ollama"; - inherit (config.ghaf.users.accounts) user; + user = "ollama"; group = "ollama"; mode = "u=rwx,g=,o="; } ]; - users.${config.ghaf.users.accounts.user}.directories = [ - ".cache" - ".config" - ".local" - "Pictures" - "Videos" - ]; }; + + # Services + + # Create launchers for regular apps running in the GUIVM and virtualized ones if GIVC is enabled + graphics.launchers = guivmLaunchers ++ lib.optionals config.ghaf.givc.enable virtualLaunchers; + graphics.labwc = { + autolock.enable = lib.mkDefault config.ghaf.graphics.labwc.autolock.enable; + autologinUser = lib.mkDefault config.ghaf.graphics.labwc.autologinUser; + securityContext = map (vm: { + identifier = vm.name; + color = vm.borderColor; + }) config.ghaf.virtualization.microvm.appvm.vms; + }; + logging.client.enable = config.ghaf.logging.client.enable; + logging.client.endpoint = config.ghaf.logging.client.endpoint; services.disks.enable = true; services.disks.fileManager = "${pkgs.pcmanfm}/bin/pcmanfm"; services.xdghandlers.enable = true; @@ -141,7 +147,7 @@ let # Switch off display, if wayland is running if ${pkgs.procps}/bin/pgrep -fl "wayland" > /dev/null; then wl_running=1 - WAYLAND_DISPLAY=/run/user/${builtins.toString config.ghaf.users.accounts.uid}/wayland-0 ${pkgs.wlopm}/bin/wlopm --off '*' + WAYLAND_DISPLAY=/run/user/${builtins.toString config.ghaf.users.loginUser.uid}/wayland-0 ${pkgs.wlopm}/bin/wlopm --off '*' else wl_running=0 fi @@ -151,7 +157,7 @@ let # Enable display if [ "$wl_running" -eq 1 ]; then - WAYLAND_DISPLAY=/run/user/${builtins.toString config.ghaf.users.accounts.uid}/wayland-0 ${pkgs.wlopm}/bin/wlopm --on '*' + WAYLAND_DISPLAY=/run/user/${builtins.toString config.ghaf.users.loginUser.uid}/wayland-0 ${pkgs.wlopm}/bin/wlopm --on '*' fi ;; "button/lid LID open") @@ -163,12 +169,15 @@ let systemd.services."waypipe-ssh-keygen" = let + uid = "${toString config.ghaf.users.loginUser.uid}"; + pubDir = config.ghaf.security.sshKeys.waypipeSshPublicKeyDir; keygenScript = pkgs.writeShellScriptBin "waypipe-ssh-keygen" '' set -xeuo pipefail mkdir -p /run/waypipe-ssh echo -en "\n\n\n" | ${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f /run/waypipe-ssh/id_ed25519 -C "" - chown ghaf:ghaf /run/waypipe-ssh/* - cp /run/waypipe-ssh/id_ed25519.pub /run/waypipe-ssh-public-key/id_ed25519.pub + chown ${uid}:users /run/waypipe-ssh/* + cp /run/waypipe-ssh/id_ed25519.pub ${pubDir}/id_ed25519.pub + chown -R ${uid}:users ${pubDir} ''; in { @@ -234,7 +243,7 @@ let hypervisor = "qemu"; shares = [ { - tag = "rw-waypipe-ssh-public-key"; + tag = "waypipe-ssh-public-key"; source = config.ghaf.security.sshKeys.waypipeSshPublicKeyDir; mountPoint = config.ghaf.security.sshKeys.waypipeSshPublicKeyDir; proto = "virtiofs"; @@ -382,27 +391,5 @@ in imports = guivmBaseConfiguration.imports ++ cfg.extraModules; }; }; - - # This directory needs to be created before any of the microvms start. - systemd.services."create-waypipe-ssh-public-key-directory" = - let - script = pkgs.writeShellScriptBin "create-waypipe-ssh-public-key-directory" '' - mkdir -pv ${config.ghaf.security.sshKeys.waypipeSshPublicKeyDir} - chown -v microvm ${config.ghaf.security.sshKeys.waypipeSshPublicKeyDir} - ''; - in - { - enable = true; - description = "Create shared directory on host"; - path = [ ]; - wantedBy = [ "microvms.target" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - StandardOutput = "journal"; - StandardError = "journal"; - ExecStart = "${script}/bin/create-waypipe-ssh-public-key-directory"; - }; - }; }; } diff --git a/modules/microvm/virtualization/microvm/idsvm/idsvm.nix b/modules/microvm/virtualization/microvm/idsvm/idsvm.nix index 4354e26c0..ab868988a 100644 --- a/modules/microvm/virtualization/microvm/idsvm/idsvm.nix +++ b/modules/microvm/virtualization/microvm/idsvm/idsvm.nix @@ -25,7 +25,6 @@ let { lib, ... }: { ghaf = { - users.accounts.enable = lib.mkDefault configHost.ghaf.users.accounts.enable; profiles.debug.enable = lib.mkDefault configHost.ghaf.profiles.debug.enable; virtualization.microvm.idsvm.mitmproxy.enable = diff --git a/modules/microvm/virtualization/microvm/microvm-host.nix b/modules/microvm/virtualization/microvm/microvm-host.nix index e0a129a43..c5944c0b0 100644 --- a/modules/microvm/virtualization/microvm/microvm-host.nix +++ b/modules/microvm/virtualization/microvm/microvm-host.nix @@ -9,6 +9,14 @@ }: let cfg = config.ghaf.virtualization.microvm-host; + inherit (lib) + mkEnableOption + mkOption + mkIf + mkMerge + types + ; + has_remove_pci_device = config.ghaf.hardware.definition.audio.removePciDevice != null; has_rescan_pci_device = config.ghaf.hardware.definition.audio.rescanPciDevice != null; has_acpi_path = config.ghaf.hardware.definition.audio.acpiPath != null; @@ -17,6 +25,7 @@ let config.ghaf.hardware.definition.audio.rescanPciDevice else config.ghaf.hardware.definition.audio.removePciDevice; + in { imports = [ @@ -24,27 +33,28 @@ in inputs.self.nixosModules.givc-host ]; options.ghaf.virtualization.microvm-host = { - enable = lib.mkEnableOption "MicroVM Host"; - networkSupport = lib.mkEnableOption "Network support services to run host applications."; + enable = mkEnableOption "MicroVM Host"; + networkSupport = mkEnableOption "Network support services to run host applications."; sharedVmDirectory = { - enable = lib.mkEnableOption "shared directory" // { + enable = mkEnableOption "shared directory" // { default = true; }; - vms = lib.mkOption { + vms = mkOption { description = '' List of names of virtual machines for which unsafe shared folder will be enabled. ''; - type = lib.types.listOf lib.types.str; + type = types.listOf types.str; default = [ ]; }; }; }; - config = lib.mkMerge [ - (lib.mkIf cfg.enable { + config = mkMerge [ + (mkIf cfg.enable { microvm.host.enable = true; microvm.host.useNotifySockets = true; + ghaf.systemd = { withName = "host-systemd"; enable = true; @@ -90,7 +100,7 @@ in }; }) - (lib.mkIf cfg.sharedVmDirectory.enable { + (mkIf cfg.sharedVmDirectory.enable { ghaf.virtualization.microvm.guivm.extraModules = [ (import ./common/shared-directory.nix "") ]; # Create directories required for sharing files with correct permissions. @@ -98,15 +108,50 @@ in let vmDirs = map ( n: - "d /storagevm/shared/shares/Unsafe\\x20${n}\\x20share/ 0700 ${config.ghaf.users.accounts.user} users" + "d /storagevm/shared/shares/Unsafe\\x20${n}\\x20share/ 0760 ${toString config.ghaf.users.loginUser.uid} users" ) cfg.sharedVmDirectory.vms; in [ "d /storagevm/shared 0755 root root" - "d /storagevm/shared/shares 0700 ${config.ghaf.users.accounts.user} users" + "d /storagevm/shared/shares 0760 ${toString config.ghaf.users.loginUser.uid} users" ] ++ vmDirs; - }) + (mkIf config.ghaf.profiles.debug.enable { + # Host service to remove user + systemd.services.remove-users = + let + userRemovalScript = pkgs.writeShellApplication { + name = "remove-users"; + runtimeInputs = [ + pkgs.coreutils + ]; + text = '' + echo "Removing ghaf login user data" + rm -r /storagevm/homes/* + rm -r /storagevm/gui-vm/var/ + echo "All ghaf login user data removed" + ''; + }; + in + mkIf config.ghaf.profiles.debug.enable { + description = "Remove ghaf login users"; + enable = true; + path = [ userRemovalScript ]; + unitConfig.ConditionPathExists = "/storagevm/gui-vm/var/lib/nixos/user.lock"; + serviceConfig = { + Type = "oneshot"; + StandardOutput = "journal"; + StandardError = "journal"; + ExecStart = "${userRemovalScript}/bin/remove-users"; + }; + }; + }) + { + # Add host directory for persistent home images + systemd.tmpfiles.rules = [ + "d /storagevm/homes 0770 microvm kvm -" + ]; + } ]; } diff --git a/modules/microvm/virtualization/microvm/modules.nix b/modules/microvm/virtualization/microvm/modules.nix index 63b7fa852..6079968f5 100644 --- a/modules/microvm/virtualization/microvm/modules.nix +++ b/modules/microvm/virtualization/microvm/modules.nix @@ -79,6 +79,12 @@ let }; }; + # User account settings + managedUserAccounts = { + config.ghaf.users.admin = config.ghaf.users.admin; + config.ghaf.users.managed = config.ghaf.users.managed; + }; + # Reference services module referenceServiceModule = { config.ghaf = optionalAttrs (hasAttr "reference" config.ghaf) { @@ -139,6 +145,7 @@ in serviceModules.wifi serviceModules.givc referenceServiceModule + managedUserAccounts ]; # Audiovm modules audiovm.extraModules = optionals cfg.audiovm.enable [ @@ -149,6 +156,7 @@ in serviceModules.audio serviceModules.givc serviceModules.bluetooth + managedUserAccounts ]; # Guivm modules guivm.extraModules = optionals cfg.guivm.enable [ @@ -163,9 +171,16 @@ in serviceModules.commonNamespace serviceModules.givc referenceProgramsModule + managedUserAccounts + ]; + adminvm.extraModules = optionals cfg.adminvm.enable [ + serviceModules.givc + managedUserAccounts + ]; + appvm.extraModules = optionals cfg.appvm.enable [ + serviceModules.givc + managedUserAccounts ]; - adminvm.extraModules = optionals cfg.adminvm.enable [ serviceModules.givc ]; - appvm.extraModules = optionals cfg.appvm.enable [ serviceModules.givc ]; }; }; } diff --git a/modules/microvm/virtualization/microvm/netvm.nix b/modules/microvm/virtualization/microvm/netvm.nix index 9fa2830a5..e5c0b6500 100644 --- a/modules/microvm/virtualization/microvm/netvm.nix +++ b/modules/microvm/virtualization/microvm/netvm.nix @@ -4,20 +4,12 @@ { config, lib, - pkgs, ... }: let vmName = "net-vm"; macAddress = "02:00:00:01:01:01"; - isGuiVmEnabled = config.ghaf.virtualization.microvm.guivm.enable; - - sshKeysHelper = pkgs.callPackage ../../../../packages/ssh-keys-helper { - inherit pkgs; - inherit config; - }; - netvmBaseConfiguration = { imports = [ inputs.impermanence.nixosModules.impermanence @@ -43,7 +35,7 @@ let imports = [ ../../../common ]; ghaf = { - users.accounts.enable = lib.mkDefault config.ghaf.users.accounts.enable; + # Profiles profiles.debug.enable = lib.mkDefault config.ghaf.profiles.debug.enable; development = { # NOTE: SSH port also becomes accessible on the network interface @@ -52,6 +44,16 @@ let debug.tools.enable = lib.mkDefault config.ghaf.development.debug.tools.enable; nix-setup.enable = lib.mkDefault config.ghaf.development.nix-setup.enable; }; + users = { + proxyUser = { + enable = true; + extraGroups = [ + "networkmanager" + ]; + }; + }; + + # System systemd = { enable = true; withName = "netvm-systemd"; @@ -63,14 +65,19 @@ let withHardenedConfigs = true; }; givc.netvm.enable = true; - # Logging client configuration - logging.client.enable = config.ghaf.logging.client.enable; - logging.client.endpoint = config.ghaf.logging.client.endpoint; + + # Storage storagevm = { enable = true; - name = "netvm"; + name = vmName; directories = [ "/etc/NetworkManager/system-connections/" ]; }; + + # Services + # Logging client configuration + logging.client.enable = config.ghaf.logging.client.enable; + logging.client.endpoint = config.ghaf.logging.client.endpoint; + }; time.timeZone = config.time.timeZone; @@ -86,8 +93,6 @@ let firewall.allowedUDPPorts = [ 53 ]; }; - services.openssh = config.ghaf.security.sshKeys.sshAuthorizedKeysCommand; - # WORKAROUND: Create a rule to temporary hardcode device name for Wi-Fi adapter on x86 # TODO this is a dirty hack to guard against adding this to Nvidia/vm targets which # dont have that definition structure yet defined. FIXME. @@ -100,24 +105,14 @@ let # Optimize is disabled because when it is enabled, qemu is built without libusb optimize.enable = false; hypervisor = "qemu"; - shares = - [ - { - tag = "ro-store"; - source = "/nix/store"; - mountPoint = "/nix/.ro-store"; - proto = "virtiofs"; - } - ] - ++ lib.optionals isGuiVmEnabled [ - { - # Add the waypipe-ssh public key to the microvm - tag = config.ghaf.security.sshKeys.waypipeSshPublicKeyName; - source = config.ghaf.security.sshKeys.waypipeSshPublicKeyDir; - mountPoint = config.ghaf.security.sshKeys.waypipeSshPublicKeyDir; - proto = "virtiofs"; - } - ]; + shares = [ + { + tag = "ro-store"; + source = "/nix/store"; + mountPoint = "/nix/.ro-store"; + proto = "virtiofs"; + } + ]; writableStoreOverlay = lib.mkIf config.ghaf.development.debug.tools.enable "/nix/.rw-store"; qemu = { @@ -134,18 +129,6 @@ let ]; }; }; - - fileSystems = lib.mkIf isGuiVmEnabled { - ${config.ghaf.security.sshKeys.waypipeSshPublicKeyDir}.options = [ "ro" ]; - }; - - # SSH is very picky about to file permissions and ownership and will - # accept neither direct path inside /nix/store or symlink that points - # there. Therefore we copy the file to /etc/ssh/get-auth-keys (by - # setting mode), instead of symlinking it. - environment.etc = lib.mkIf isGuiVmEnabled { - ${config.ghaf.security.sshKeys.getAuthKeysFilePathInEtc} = sshKeysHelper.getAuthKeysSource; - }; } ) ]; diff --git a/modules/reference/personalize/accounts.nix b/modules/reference/personalize/accounts.nix new file mode 100644 index 000000000..c1b79d191 --- /dev/null +++ b/modules/reference/personalize/accounts.nix @@ -0,0 +1,44 @@ +# Copyright 2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ config, ... }: +{ + # Account management file for ghaf, allows to declaratively manage user accounts. + # Main use-case is to centrally administer passwords across builds. + # The admin account defaults to true even without this setting as not all + # targets use the modules interface yet. + + config = { + ghaf.users = { + # Default admin account + admin = { + enable = true; + initialPassword = "ghaf"; + initialHashedPassword = null; + hashedPassword = null; + }; + # Example of a managed user account + # managed = [ + # { + # name = "some-user"; + # vms = [ + # "ghaf-host" + # "audio-vm" + # ]; + # initialPassword = null; + # initialHashedPassword = null; + # hashedPassword = "$y$j9T$SiiIgN1tg8NadUt.YfeT01$Td60m8AC4F8DTFlxkurJ.G8i6lCfit5A7GHal8S49S9"; + # } + # { + # name = "some-ui-user"; + # vms = [ + # "gui-vm" + # ]; + # initialPassword = null; + # initialHashedPassword = null; + # hashedPassword = "$y$j9T$SiiIgN1tg8NadUt.YfeT01$Td60m8AC4F8DTFlxkurJ.G8i6lCfit5A7GHal8S49S9"; + # uid = 1002; + # } + # ]; + }; + }; +} diff --git a/modules/reference/personalize/default.nix b/modules/reference/personalize/default.nix index c967c24f3..39be35476 100644 --- a/modules/reference/personalize/default.nix +++ b/modules/reference/personalize/default.nix @@ -1,3 +1,8 @@ # Copyright 2024 TII (SSRC) and the Ghaf contributors # SPDX-License-Identifier: Apache-2.0 -{ imports = [ ./keys.nix ]; } +{ + imports = [ + ./keys.nix + ./accounts.nix + ]; +} diff --git a/modules/reference/personalize/keys.nix b/modules/reference/personalize/keys.nix index 6058c2263..b3e23e19d 100644 --- a/modules/reference/personalize/keys.nix +++ b/modules/reference/personalize/keys.nix @@ -31,7 +31,7 @@ in config = mkIf cfg.enable { users.users.root.openssh.authorizedKeys.keys = authorizedSshKeys; - users.users.${config.ghaf.users.accounts.user}.openssh.authorizedKeys.keys = authorizedSshKeys; + users.users.${config.ghaf.users.admin.name}.openssh.authorizedKeys.keys = authorizedSshKeys; ghaf.services.yubikey.u2fKeys = mkForce (concatStrings authorizedYubikeys); }; } diff --git a/packages/ghaf-powercontrol/default.nix b/packages/ghaf-powercontrol/default.nix index bd008830a..dfc4ba474 100644 --- a/packages/ghaf-powercontrol/default.nix +++ b/packages/ghaf-powercontrol/default.nix @@ -24,7 +24,7 @@ let useGivc = ghafConfig.givc.enable; # Handle Wayland display power state waylandDisplayCmd = command: '' - WAYLAND_DISPLAY=/run/user/${builtins.toString ghafConfig.users.accounts.uid}/wayland-0 \ + WAYLAND_DISPLAY=/run/user/${builtins.toString ghafConfig.users.loginUser.uid}/wayland-0 \ wlopm --${command} '*' ''; in diff --git a/packages/ghaf-xdg-open/default.nix b/packages/ghaf-xdg-open/default.nix index 47ad36850..8196e52b3 100644 --- a/packages/ghaf-xdg-open/default.nix +++ b/packages/ghaf-xdg-open/default.nix @@ -6,6 +6,7 @@ dnsutils, openssh, sshKeyPath, + user, ... }: # This script is executed in the GUIVM by the Ghaf XDG systemd service when it receives an XDG open request. @@ -26,7 +27,6 @@ writeShellApplication { businessvmip=$(dig +short business-vm | head -1) commsvmip=$(dig +short comms-vm | head -1) - if [[ "127.0.0.1" != "$REMOTE_ADDR" && \ "$businessvmip" != "$REMOTE_ADDR" && \ "$googlechromevmip" != "$REMOTE_ADDR" && \ @@ -37,23 +37,23 @@ writeShellApplication { if [[ "127.0.0.1" != "$REMOTE_ADDR" ]]; then echo "Copying $sourcepath from $REMOTE_ADDR to $zathurapath in zathura-vm" - scp -i ${sshKeyPath} -o StrictHostKeyChecking=no "$REMOTE_ADDR":"$sourcepath" zathura-vm:"$zathurapath" + scp -i ${sshKeyPath} -o StrictHostKeyChecking=no ${user}@"$REMOTE_ADDR":"$sourcepath" ${user}@zathura-vm:"$zathurapath" else echo "Copying $sourcepath from GUIVM to $zathurapath in zathura-vm" - scp -i ${sshKeyPath} -o StrictHostKeyChecking=no "$sourcepath" zathura-vm:"$zathurapath" + scp -i ${sshKeyPath} -o StrictHostKeyChecking=no "$sourcepath" ${user}@zathura-vm:"$zathurapath" fi echo "Opening $zathurapath in zathura-vm" if [[ "$type" == "pdf" ]]; then - ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no zathura-vm run-waypipe zathura "'$zathurapath'" + ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no ${user}@zathura-vm run-waypipe zathura "'$zathurapath'" elif [[ "$type" == "image" ]]; then - ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no zathura-vm run-waypipe pqiv -i "'$zathurapath'" + ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no ${user}@zathura-vm run-waypipe pqiv -i "'$zathurapath'" else echo "Unknown type: $type" fi echo "Deleting $zathurapath in zathura-vm" - ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no zathura-vm rm -f "$zathurapath" + ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no ${user}@zathura-vm rm -f "$zathurapath" ''; } diff --git a/packages/ssh-keys-helper/default.nix b/packages/ssh-keys-helper/default.nix index e0dabb719..9ebe05c77 100644 --- a/packages/ssh-keys-helper/default.nix +++ b/packages/ssh-keys-helper/default.nix @@ -6,7 +6,7 @@ source = let script = pkgs.writeShellScriptBin config.ghaf.security.sshKeys.getAuthKeysFileName '' - [[ "$1" != "ghaf" ]] && exit 0 + [[ "$1" != "${config.ghaf.users.appUser.name}" && "$1" != "${config.ghaf.users.admin.name}" ]] && exit 0 ${pkgs.coreutils}/bin/cat ${config.ghaf.security.sshKeys.waypipeSshPublicKeyFile} ''; in diff --git a/packages/wifi-signal-strength/default.nix b/packages/wifi-signal-strength/default.nix deleted file mode 100644 index 427e875a3..000000000 --- a/packages/wifi-signal-strength/default.nix +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright 2022-2024 TII (SSRC) and the Ghaf contributors -# SPDX-License-Identifier: Apache-2.0 -{ - networkmanager, - openssh, - util-linux, - gawk, - coreutils-full, - writeShellApplication, - wifiDevice, - ... -}: -writeShellApplication { - name = "wifi-signal-strength"; - runtimeInputs = [ - networkmanager - openssh - gawk - util-linux - coreutils-full - ]; - text = '' - NETWORK_STATUS_FILE=/tmp/network-status - - export DBUS_SESSION_BUS_ADDRESS=unix:path=/tmp/ssh_session_dbus.sock - export DBUS_SYSTEM_BUS_ADDRESS=unix:path=/tmp/ssh_system_dbus.sock - - # Lock the script to reuse - LOCK_FILE=/tmp/wifi-signal.lock - exec 99>"$LOCK_FILE" - flock -w 60 -x 99 || exit 1 - - # Return the result as json format for waybar and use the control socket to close the ssh tunnel. - trap 'ssh -q -S /tmp/nmcli_socket -O exit ghaf@net-vm && cat "$NETWORK_STATUS_FILE"' EXIT - - # Connect to netvm - ssh -M -S /tmp/nmcli_socket \ - -f -N -q ghaf@net-vm \ - -i /run/waypipe-ssh/id_ed25519 \ - -o StrictHostKeyChecking=no \ - -o UserKnownHostsFile=/dev/null \ - -o StreamLocalBindUnlink=yes \ - -o ExitOnForwardFailure=yes \ - -L /tmp/ssh_session_dbus.sock:/run/user/${builtins.toString config.ghaf.users.accounts.uid}/bus \ - -L /tmp/ssh_system_dbus.sock:/run/dbus/system_bus_socket - signal0="\UF091F" - signal1="\UF0922" - signal2="\UF0925" - signal3="\UF0928" - no_signal="\UF092D" - # Get IP address of netvm - address=$(nmcli device show ${wifiDevice} | awk '{ if ($1=="IP4.ADDRESS[1]:") {print $2}}') - # Get signal strength and ssi - mapfile -t connection < <(nmcli -f IN-USE,SIGNAL,SSID dev wifi | awk '/^\*/{if (NR!=1) {print $2; print $3}}') - connection[0]=$(if [ -z "''${connection[0]}" ]; then echo "-1"; else echo "''${connection[0]}"; fi) - # Set the icon of signal level - signal_level=$(if [ "''${connection[0]}" -gt 80 ]; then echo "''${signal3}"; elif [ "''${connection[0]}" -gt 60 ]; then echo "''${signal2}"; elif [ "''${connection[0]}" -gt 30 ]; then echo "''${signal1}"; elif [ "''${connection[0]}" -gt 0 ]; then echo "''${signal0};" else echo "''${no_signal}"; fi) - tooltip=$(if [ -z "''${address}" ]; then echo "''${connection[0]}%"; else echo "''${address} ''${connection[0]}%"; fi) - text=$(if [ -z "''${connection[1]}" ]; then echo "No connection"; else echo "''${connection[1]} $signal_level"; fi) - # Save the result in json format - RESULT="{\"percentage\":\"''${connection[0]}\", \"text\":\"''${text}\", \"tooltip\":\"''${tooltip}\", \"class\":\"1\"}" - echo -e "$RESULT">/tmp/network-status - flock -u 99 - ''; -}