diff --git a/modules/misc/news.nix b/modules/misc/news.nix index 7854e5da9..c13b0b8f1 100644 --- a/modules/misc/news.nix +++ b/modules/misc/news.nix @@ -1356,6 +1356,13 @@ in A new module is available: 'services.osmscout-server'. ''; } + + { + time = "2023-12-28T13:01:15+00:00"; + message = '' + A new module is available: 'programs.sftpman'. + ''; + } ]; }; } diff --git a/modules/modules.nix b/modules/modules.nix index 993c77adf..96a9b0b89 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -198,6 +198,7 @@ let ./programs/scmpuff.nix ./programs/script-directory.nix ./programs/senpai.nix + ./programs/sftpman.nix ./programs/sioyek.nix ./programs/skim.nix ./programs/sm64ex.nix diff --git a/modules/programs/sftpman.nix b/modules/programs/sftpman.nix new file mode 100644 index 000000000..c6978f80a --- /dev/null +++ b/modules/programs/sftpman.nix @@ -0,0 +1,118 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.programs.sftpman; + + jsonFormat = pkgs.formats.json { }; + + mountOpts = { config, name, ... }: { + options = { + host = mkOption { + type = types.str; + description = "The host to connect to."; + }; + + port = mkOption { + type = types.port; + default = 22; + description = "The port to connect to."; + }; + + user = mkOption { + type = types.str; + description = "The username to authenticate with."; + }; + + mountOptions = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Options to pass to sshfs."; + }; + + mountPoint = mkOption { + type = types.str; + description = "The remote path to mount."; + }; + + authType = mkOption { + type = types.enum [ + "password" + "publickey" + "hostbased" + "keyboard-interactive" + "gssapi-with-mic" + ]; + default = "publickey"; + description = "The authentication method to use."; + }; + + sshKey = mkOption { + type = types.nullOr types.str; + default = cfg.defaultSshKey; + defaultText = + lib.literalExpression "config.programs.sftpman.defaultSshKey"; + description = '' + Path to the SSH key to use for authentication. + Only applies if authMethod is `publickey`. + ''; + }; + + beforeMount = mkOption { + type = types.str; + default = "true"; + description = "Command to run before mounting."; + }; + }; + }; +in { + meta.maintainers = with maintainers; [ fugi ]; + + options.programs.sftpman = { + enable = mkEnableOption + "sftpman, an application that handles sshfs/sftp file systems mounting"; + + package = mkPackageOption pkgs "sftpman" { }; + + defaultSshKey = mkOption { + type = types.nullOr types.str; + default = null; + description = + "Path to the SSH key to be used by default. Can be overridden per host."; + }; + + mounts = mkOption { + type = types.attrsOf (types.submodule mountOpts); + default = { }; + description = '' + The sshfs mount configurations written to + {file}`$XDG_CONFIG_HOME/sftpman/mounts/`. + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + (let + hasMissingKey = _: mount: + mount.authType == "publickey" && mount.sshKey == null; + mountsWithMissingKey = attrNames (filterAttrs hasMissingKey cfg.mounts); + mountsWithMissingKeyStr = concatStringsSep ", " mountsWithMissingKey; + in { + assertion = mountsWithMissingKey == [ ]; + message = '' + sftpman mounts using authentication type "publickey" but missing 'sshKey': ${mountsWithMissingKeyStr} + ''; + }) + ]; + + home.packages = [ cfg.package ]; + + xdg.configFile = mapAttrs' (name: value: + nameValuePair "sftpman/mounts/${name}.json" { + source = + jsonFormat.generate "sftpman-${name}.json" (value // { id = name; }); + }) cfg.mounts; + }; +} diff --git a/tests/default.nix b/tests/default.nix index 53458adba..c7b7892c3 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -139,6 +139,7 @@ import nmt { ./modules/programs/sapling ./modules/programs/sbt ./modules/programs/scmpuff + ./modules/programs/sftpman ./modules/programs/sioyek ./modules/programs/sm64ex ./modules/programs/ssh diff --git a/tests/modules/programs/sftpman/assert-on-no-sshkey.nix b/tests/modules/programs/sftpman/assert-on-no-sshkey.nix new file mode 100644 index 000000000..549b68902 --- /dev/null +++ b/tests/modules/programs/sftpman/assert-on-no-sshkey.nix @@ -0,0 +1,21 @@ +{ + config = { + programs.sftpman = { + enable = true; + + mounts = { + mount1 = { + host = "host1.example.com"; + mountPoint = "/path/to/somewhere"; + user = "root"; + }; + }; + }; + + test.stubs.sftpman = { }; + + test.asserts.assertions.expected = ['' + sftpman mounts using authentication type "publickey" but missing 'sshKey': mount1 + '']; + }; +} diff --git a/tests/modules/programs/sftpman/default.nix b/tests/modules/programs/sftpman/default.nix new file mode 100644 index 000000000..55e319e16 --- /dev/null +++ b/tests/modules/programs/sftpman/default.nix @@ -0,0 +1,4 @@ +{ + sftpman-example-settings = ./example-settings.nix; + sftpman-assert-on-no-sshkey = ./assert-on-no-sshkey.nix; +} diff --git a/tests/modules/programs/sftpman/example-settings.nix b/tests/modules/programs/sftpman/example-settings.nix new file mode 100644 index 000000000..265f35525 --- /dev/null +++ b/tests/modules/programs/sftpman/example-settings.nix @@ -0,0 +1,44 @@ +{ + config = { + programs.sftpman = { + enable = true; + defaultSshKey = "/home/user/.ssh/id_ed25519"; + + mounts = { + mount1 = { + host = "host1.example.com"; + mountPoint = "/path/to/somewhere"; + user = "root"; + mountOptions = [ "idmap=user" ]; + }; + mount2 = { + host = "host2.example.com"; + mountPoint = "/another/path"; + user = "someuser"; + authType = "password"; + sshKey = null; + }; + mount3 = { + host = "host3.example.com"; + mountPoint = "/yet/another/path"; + user = "user"; + sshKey = "/home/user/.ssh/id_rsa"; + }; + }; + }; + + test.stubs.sftpman = { }; + + nmt.script = '' + assertFileContent \ + home-files/.config/sftpman/mounts/mount1.json \ + ${./expected-mount1.json} + assertFileContent \ + home-files/.config/sftpman/mounts/mount2.json \ + ${./expected-mount2.json} + assertFileContent \ + home-files/.config/sftpman/mounts/mount3.json \ + ${./expected-mount3.json} + ''; + }; +} diff --git a/tests/modules/programs/sftpman/expected-mount1.json b/tests/modules/programs/sftpman/expected-mount1.json new file mode 100644 index 000000000..06124fd26 --- /dev/null +++ b/tests/modules/programs/sftpman/expected-mount1.json @@ -0,0 +1,13 @@ +{ + "authType": "publickey", + "beforeMount": "true", + "host": "host1.example.com", + "id": "mount1", + "mountOptions": [ + "idmap=user" + ], + "mountPoint": "/path/to/somewhere", + "port": 22, + "sshKey": "/home/user/.ssh/id_ed25519", + "user": "root" +} diff --git a/tests/modules/programs/sftpman/expected-mount2.json b/tests/modules/programs/sftpman/expected-mount2.json new file mode 100644 index 000000000..384a9e93f --- /dev/null +++ b/tests/modules/programs/sftpman/expected-mount2.json @@ -0,0 +1,11 @@ +{ + "authType": "password", + "beforeMount": "true", + "host": "host2.example.com", + "id": "mount2", + "mountOptions": [], + "mountPoint": "/another/path", + "port": 22, + "sshKey": null, + "user": "someuser" +} diff --git a/tests/modules/programs/sftpman/expected-mount3.json b/tests/modules/programs/sftpman/expected-mount3.json new file mode 100644 index 000000000..5ee679623 --- /dev/null +++ b/tests/modules/programs/sftpman/expected-mount3.json @@ -0,0 +1,11 @@ +{ + "authType": "publickey", + "beforeMount": "true", + "host": "host3.example.com", + "id": "mount3", + "mountOptions": [], + "mountPoint": "/yet/another/path", + "port": 22, + "sshKey": "/home/user/.ssh/id_rsa", + "user": "user" +}