diff --git a/modules/services/proton-pass-agent.nix b/modules/services/proton-pass-agent.nix new file mode 100644 index 00000000..f098e360 --- /dev/null +++ b/modules/services/proton-pass-agent.nix @@ -0,0 +1,143 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.proton-pass-agent; +in +{ + meta.maintainers = [ lib.maintainers.delafthi ]; + + options.services.proton-pass-agent = { + enable = lib.mkEnableOption "Proton Pass as a SSH agent"; + + package = lib.mkPackageOption pkgs "proton-pass-cli" { }; + + socket = lib.mkOption { + type = lib.types.str; + default = "proton-pass-agent"; + example = "proton-pass-agent/socket"; + description = '' + The agent's socket; interpreted as a suffix to {env}`$XDG_RUNTIME_DIR` + on Linux and `$(getconf DARWIN_USER_TEMP_DIR)` on macOS. This option + adds the `--socket-path` argument to the command. + ''; + }; + + extraArgs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ + "--share-id" + "--vault-name" + "MySshKeyVault" + "--refresh-interval" + "7200" + ]; + description = '' + Options given to `pass-cli ssh-agent shart` when the service is run. + + See + for more information. + ''; + }; + + enableBashIntegration = lib.hm.shell.mkBashIntegrationOption { inherit config; }; + + enableZshIntegration = lib.hm.shell.mkZshIntegrationOption { inherit config; }; + + enableFishIntegration = lib.hm.shell.mkFishIntegrationOption { inherit config; }; + + enableNushellIntegration = lib.hm.shell.mkNushellIntegrationOption { inherit config; }; + }; + + config = + let + socketPath = + if pkgs.stdenv.isDarwin then + "$(${lib.getExe pkgs.getconf} DARWIN_USER_TEMP_DIR)/${cfg.socket}" + else + "$XDG_RUNTIME_DIR/${cfg.socket}"; + cmd = [ + "${lib.getExe' cfg.package "pass-cli"}" + "ssh-agent" + "start" + "--socket-path" + "${if pkgs.stdenv.isDarwin then socketPath else "%t/${cfg.socket}"}" + ] + ++ cfg.extraArgs; + in + lib.mkIf cfg.enable { + home.packages = [ cfg.package ]; + + programs = + let + # Preserve $SSH_AUTH_SOCK only if it stems from a forwarded agent which + # is the case if both $SSH_AUTH_SOCK and $SSH_CONNECTION are set. + bashIntegration = '' + if [ -z "$SSH_AUTH_SOCK" -o -z "$SSH_CONNECTION" ]; then + export SSH_AUTH_SOCK=${socketPath} + fi + ''; + fishIntegration = '' + if test -z "$SSH_AUTH_SOCK"; or test -z "$SSH_CONNECTION" + set -x SSH_AUTH_SOCK ${socketPath} + end + ''; + nushellIntegration = + let + unsetOrEmpty = var: ''("${var}" not-in $env) or ($env.${var} | is-empty)''; + socketPath = + if pkgs.stdenv.isDarwin then + ''$"(${lib.getExe pkgs.getconf} DARWIN_USER_TEMP_DIR)/${cfg.socket}"'' + else + ''$"($env.XDG_RUNTIME_DIR)/${cfg.socket}"''; + in + '' + if ${unsetOrEmpty "SSH_AUTH_SOCK"} or ${unsetOrEmpty "SSH_CONNECTION"} { + $env.SSH_AUTH_SOCK = ${socketPath} + } + ''; + in + { + # $SSH_AUTH_SOCK has to be set early since other tools rely on it + bash.profileExtra = lib.mkIf cfg.enableBashIntegration (lib.mkOrder 900 bashIntegration); + fish.shellInit = lib.mkIf cfg.enableFishIntegration (lib.mkOrder 900 fishIntegration); + nushell.extraConfig = lib.mkIf cfg.enableNushellIntegration (lib.mkOrder 900 nushellIntegration); + zsh.envExtra = lib.mkIf cfg.enableZshIntegration (lib.mkOrder 900 bashIntegration); + }; + + systemd.user.services.proton-pass-agent = { + Install.WantedBy = [ "default.target" ]; + Unit = { + Description = "Proton Pass SSH agent"; + Documentation = "https://protonpass.github.io/pass-cli/commands/ssh-agent/#ssh-agent-integration"; + }; + Service = { + ExecStart = lib.concatStringsSep " " cmd; + KeyringMode = "shared"; + }; + }; + + launchd.agents.proton-pass-agent = { + enable = true; + config = { + ProgramArguments = [ + (lib.getExe pkgs.bash) + "-c" + (lib.concatStringsSep " " cmd) + ]; + KeepAlive = { + Crashed = true; + SuccessfulExit = false; + }; + ProcessType = "Background"; + RunAtLoad = true; + StandardOutPath = "${config.home.homeDirectory}/Library/Logs/Proton Pass CLI/ssh-agent-stdout.log"; + StandardErrorPath = "${config.home.homeDirectory}/Library/Logs/Proton Pass CLI/ssh-agent-stderr.log"; + }; + }; + }; +} diff --git a/tests/darwinScrublist.nix b/tests/darwinScrublist.nix index ec8cafd3..9487b456 100644 --- a/tests/darwinScrublist.nix +++ b/tests/darwinScrublist.nix @@ -140,6 +140,7 @@ let "pls" "poetry" "powerline-go" + "proton-pass-cli" "pubs" "pyenv" "qcal" diff --git a/tests/modules/services/proton-pass-agent/bash-integration.nix b/tests/modules/services/proton-pass-agent/bash-integration.nix new file mode 100644 index 00000000..7f30cccb --- /dev/null +++ b/tests/modules/services/proton-pass-agent/bash-integration.nix @@ -0,0 +1,23 @@ +{ pkgs, ... }: +{ + services.proton-pass-agent = { + enable = true; + enableBashIntegration = true; + }; + + programs.bash.enable = true; + + nmt.script = '' + bash_profile=home-files/.profile + + assertFileContains $bash_profile \ + 'if [ -z "$SSH_AUTH_SOCK" -o -z "$SSH_CONNECTION" ]; then' + assertFileContains $bash_profile \ + 'export SSH_AUTH_SOCK=${ + if pkgs.stdenv.hostPlatform.isDarwin then + "$(@getconf-system_cmds@/bin/getconf DARWIN_USER_TEMP_DIR)" + else + "$XDG_RUNTIME_DIR" + }/proton-pass-agent' + ''; +} diff --git a/tests/modules/services/proton-pass-agent/basic-service-expected.plist b/tests/modules/services/proton-pass-agent/basic-service-expected.plist new file mode 100644 index 00000000..a4380df5 --- /dev/null +++ b/tests/modules/services/proton-pass-agent/basic-service-expected.plist @@ -0,0 +1,29 @@ + + + + + KeepAlive + + Crashed + + SuccessfulExit + + + Label + org.nix-community.home.proton-pass-agent + ProcessType + Background + ProgramArguments + + @bash-interactive@/bin/bash + -c + @proton-pass-cli@/bin/pass-cli ssh-agent start --socket-path $(@getconf-system_cmds@/bin/getconf DARWIN_USER_TEMP_DIR)/proton-pass-agent/socket + + RunAtLoad + + StandardErrorPath + /home/hm-user/Library/Logs/Proton Pass CLI/ssh-agent-stderr.log + StandardOutPath + /home/hm-user/Library/Logs/Proton Pass CLI/ssh-agent-stdout.log + + \ No newline at end of file diff --git a/tests/modules/services/proton-pass-agent/basic-service-expected.service b/tests/modules/services/proton-pass-agent/basic-service-expected.service new file mode 100644 index 00000000..af73d2f9 --- /dev/null +++ b/tests/modules/services/proton-pass-agent/basic-service-expected.service @@ -0,0 +1,10 @@ +[Install] +WantedBy=default.target + +[Service] +ExecStart=@proton-pass-cli@/bin/pass-cli ssh-agent start --socket-path %t/proton-pass-agent/socket +KeyringMode=shared + +[Unit] +Description=Proton Pass SSH agent +Documentation=https://protonpass.github.io/pass-cli/commands/ssh-agent/#ssh-agent-integration diff --git a/tests/modules/services/proton-pass-agent/basic-service.nix b/tests/modules/services/proton-pass-agent/basic-service.nix new file mode 100644 index 00000000..79b3f070 --- /dev/null +++ b/tests/modules/services/proton-pass-agent/basic-service.nix @@ -0,0 +1,23 @@ +{ config, pkgs, ... }: +{ + services.proton-pass-agent = { + enable = true; + socket = "proton-pass-agent/socket"; + }; + + nmt.script = + if pkgs.stdenv.hostPlatform.isDarwin then + '' + plistFile=LaunchAgents/org.nix-community.home.proton-pass-agent.plist + + assertFileExists $plistFile + assertFileContent $plistFile ${./basic-service-expected.plist} + '' + else + '' + serviceFile=home-files/.config/systemd/user/proton-pass-agent.service + + assertFileExists $serviceFile + assertFileContent $serviceFile ${./basic-service-expected.service} + ''; +} diff --git a/tests/modules/services/proton-pass-agent/default.nix b/tests/modules/services/proton-pass-agent/default.nix new file mode 100644 index 00000000..e74b9e84 --- /dev/null +++ b/tests/modules/services/proton-pass-agent/default.nix @@ -0,0 +1,7 @@ +{ + proton-pass-agent-basic-service = ./basic-service.nix; + proton-pass-agent-full-service = ./full-service.nix; + proton-pass-agent-bash-integration = ./bash-integration.nix; + proton-pass-agent-fish-integration = ./fish-integration.nix; + proton-pass-agent-nushell-integration = ./nushell-integration.nix; +} diff --git a/tests/modules/services/proton-pass-agent/fish-integration.nix b/tests/modules/services/proton-pass-agent/fish-integration.nix new file mode 100644 index 00000000..b22066ef --- /dev/null +++ b/tests/modules/services/proton-pass-agent/fish-integration.nix @@ -0,0 +1,23 @@ +{ pkgs, ... }: +{ + services.proton-pass-agent = { + enable = true; + enableBashIntegration = true; + }; + + programs.fish.enable = true; + + nmt.script = '' + fish_config=home-files/.config/fish/config.fish + + assertFileContains $fish_config \ + 'if test -z "$SSH_AUTH_SOCK"; or test -z "$SSH_CONNECTION' + assertFileContains $fish_config \ + 'set -x SSH_AUTH_SOCK ${ + if pkgs.stdenv.hostPlatform.isDarwin then + "$(@getconf-system_cmds@/bin/getconf DARWIN_USER_TEMP_DIR)" + else + "$XDG_RUNTIME_DIR" + }/proton-pass-agent' + ''; +} diff --git a/tests/modules/services/proton-pass-agent/full-service-expected.plist b/tests/modules/services/proton-pass-agent/full-service-expected.plist new file mode 100644 index 00000000..9d0e9690 --- /dev/null +++ b/tests/modules/services/proton-pass-agent/full-service-expected.plist @@ -0,0 +1,29 @@ + + + + + KeepAlive + + Crashed + + SuccessfulExit + + + Label + org.nix-community.home.proton-pass-agent + ProcessType + Background + ProgramArguments + + @bash-interactive@/bin/bash + -c + @proton-pass-cli@/bin/pass-cli ssh-agent start --socket-path $(@getconf-system_cmds@/bin/getconf DARWIN_USER_TEMP_DIR)/proton-pass-agent/socket --share-id 123456789 --vault-name MySshKeyVault --refresh-interval 7200 --create-new-identities MySshKeyVault + + RunAtLoad + + StandardErrorPath + /home/hm-user/Library/Logs/Proton Pass CLI/ssh-agent-stderr.log + StandardOutPath + /home/hm-user/Library/Logs/Proton Pass CLI/ssh-agent-stdout.log + + \ No newline at end of file diff --git a/tests/modules/services/proton-pass-agent/full-service-expected.service b/tests/modules/services/proton-pass-agent/full-service-expected.service new file mode 100644 index 00000000..139c683c --- /dev/null +++ b/tests/modules/services/proton-pass-agent/full-service-expected.service @@ -0,0 +1,10 @@ +[Install] +WantedBy=default.target + +[Service] +ExecStart=@proton-pass-cli@/bin/pass-cli ssh-agent start --socket-path %t/proton-pass-agent/socket --share-id 123456789 --vault-name MySshKeyVault --refresh-interval 7200 --create-new-identities MySshKeyVault +KeyringMode=shared + +[Unit] +Description=Proton Pass SSH agent +Documentation=https://protonpass.github.io/pass-cli/commands/ssh-agent/#ssh-agent-integration diff --git a/tests/modules/services/proton-pass-agent/full-service.nix b/tests/modules/services/proton-pass-agent/full-service.nix new file mode 100644 index 00000000..0316555f --- /dev/null +++ b/tests/modules/services/proton-pass-agent/full-service.nix @@ -0,0 +1,33 @@ +{ config, pkgs, ... }: +{ + services.proton-pass-agent = { + enable = true; + socket = "proton-pass-agent/socket"; + extraArgs = [ + "--share-id" + "123456789" + "--vault-name" + "MySshKeyVault" + "--refresh-interval" + "7200" + "--create-new-identities" + "MySshKeyVault" + ]; + }; + + nmt.script = + if pkgs.stdenv.hostPlatform.isDarwin then + '' + plistFile=LaunchAgents/org.nix-community.home.proton-pass-agent.plist + + assertFileExists $plistFile + assertFileContent $plistFile ${./full-service-expected.plist} + '' + else + '' + serviceFile=home-files/.config/systemd/user/proton-pass-agent.service + + assertFileExists $serviceFile + assertFileContent $serviceFile ${./full-service-expected.service} + ''; +} diff --git a/tests/modules/services/proton-pass-agent/nushell-integration.nix b/tests/modules/services/proton-pass-agent/nushell-integration.nix new file mode 100644 index 00000000..19dd2873 --- /dev/null +++ b/tests/modules/services/proton-pass-agent/nushell-integration.nix @@ -0,0 +1,27 @@ +{ pkgs, ... }: +{ + services.proton-pass-agent = { + enable = true; + enableNushellIntegration = true; + }; + + programs.nushell.enable = true; + + nmt.script = + let + unsetOrEmpty = var: ''("${var}" not-in $env) or ($env.${var} | is-empty)''; + in + '' + nu_config=home-files/.config/nushell/config.nu + + assertFileContains $nu_config \ + 'if ${unsetOrEmpty "SSH_AUTH_SOCK"} or ${unsetOrEmpty "SSH_CONNECTION"} {' + assertFileContains $nu_config \ + '$env.SSH_AUTH_SOCK = $"${ + if pkgs.stdenv.hostPlatform.isDarwin then + "(@getconf-system_cmds@/bin/getconf DARWIN_USER_TEMP_DIR)" + else + "($env.XDG_RUNTIME_DIR)" + }/proton-pass-agent"' + ''; +}