mirror of
https://github.com/NixOS/nixpkgs.git
synced 2024-11-22 15:03:28 +00:00
nixos/tests/sftpgo: init
This commit is contained in:
parent
f63f781063
commit
931a1b97f7
@ -665,6 +665,7 @@ in {
|
||||
seafile = handleTest ./seafile.nix {};
|
||||
searx = handleTest ./searx.nix {};
|
||||
service-runner = handleTest ./service-runner.nix {};
|
||||
sftpgo = runTest ./sftpgo.nix;
|
||||
sfxr-qt = handleTest ./sfxr-qt.nix {};
|
||||
sgtpuzzles = handleTest ./sgtpuzzles.nix {};
|
||||
shadow = handleTest ./shadow.nix {};
|
||||
|
384
nixos/tests/sftpgo.nix
Normal file
384
nixos/tests/sftpgo.nix
Normal file
@ -0,0 +1,384 @@
|
||||
# SFTPGo NixOS test
|
||||
#
|
||||
# This NixOS test sets up a basic test scenario for the SFTPGo module
|
||||
# and covers the following scenarios:
|
||||
# - uploading a file via sftp
|
||||
# - downloading the file over sftp
|
||||
# - assert that the ACLs are respected
|
||||
# - share a file between alice and bob (using sftp)
|
||||
# - assert that eve cannot acceess the shared folder between alice and bob.
|
||||
#
|
||||
# Additional test coverage for the remaining protocols (i.e. ftp, http and webdav)
|
||||
# would be a nice to have for the future.
|
||||
{ pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
|
||||
|
||||
# Returns an attributeset of users who are not system users.
|
||||
normalUsers = config:
|
||||
filterAttrs (name: user: user.isNormalUser) config.users.users;
|
||||
|
||||
# Returns true if a user is a member of the given group
|
||||
isMemberOf =
|
||||
config:
|
||||
# str
|
||||
groupName:
|
||||
# users.users attrset
|
||||
user:
|
||||
any (x: x == user.name) config.users.groups.${groupName}.members;
|
||||
|
||||
# Generates a valid SFTPGo user configuration for a given user
|
||||
# Will be converted to JSON and loaded on application startup.
|
||||
generateUserAttrSet =
|
||||
config:
|
||||
# attrset returned by config.users.users.<username>
|
||||
user: {
|
||||
# 0: user is disabled, login is not allowed
|
||||
# 1: user is enabled
|
||||
status = 1;
|
||||
|
||||
username = user.name;
|
||||
password = ""; # disables password authentication
|
||||
public_keys = user.openssh.authorizedKeys.keys;
|
||||
email = "${user.name}@example.com";
|
||||
|
||||
# User home directory on the local filesystem
|
||||
home_dir = "${config.services.sftpgo.dataDir}/users/${user.name}";
|
||||
|
||||
# Defines a mapping between virtual SFTPGo paths and filesystem paths outside the user home directory.
|
||||
#
|
||||
# Supported for local filesystem only. If one or more of the specified folders are not
|
||||
# inside the dataprovider they will be automatically created.
|
||||
# You have to create the folder on the filesystem yourself
|
||||
virtual_folders =
|
||||
optional (isMemberOf config sharedFolderName user) {
|
||||
name = sharedFolderName;
|
||||
mapped_path = "${config.services.sftpgo.dataDir}/${sharedFolderName}";
|
||||
virtual_path = "/${sharedFolderName}";
|
||||
};
|
||||
|
||||
# Defines the ACL on the virtual filesystem
|
||||
permissions =
|
||||
recursiveUpdate {
|
||||
"/" = [ "list" ]; # read-only top level directory
|
||||
"/private" = [ "*" ]; # private subdirectory, not shared with others
|
||||
} (optionalAttrs (isMemberOf config "shared" user) {
|
||||
"/shared" = [ "*" ];
|
||||
});
|
||||
|
||||
filters = {
|
||||
allowed_ip = [];
|
||||
denied_ip = [];
|
||||
web_client = [
|
||||
"password-change-disabled"
|
||||
"password-reset-disabled"
|
||||
"api-key-auth-change-disabled"
|
||||
];
|
||||
};
|
||||
|
||||
upload_bandwidth = 0; # unlimited
|
||||
download_bandwidth = 0; # unlimited
|
||||
expiration_date = 0; # means no expiration
|
||||
max_sessions = 0;
|
||||
quota_size = 0;
|
||||
quota_files = 0;
|
||||
};
|
||||
|
||||
# Generates a json file containing a static configuration
|
||||
# of users and folders to import to SFTPGo.
|
||||
loadDataJson = config: pkgs.writeText "users-and-folders.json" (builtins.toJSON {
|
||||
users =
|
||||
mapAttrsToList (name: user: generateUserAttrSet config user) (normalUsers config);
|
||||
|
||||
folders = [
|
||||
{
|
||||
name = sharedFolderName;
|
||||
description = "shared folder";
|
||||
|
||||
# 0: local filesystem
|
||||
# 1: AWS S3 compatible
|
||||
# 2: Google Cloud Storage
|
||||
filesystem.provider = 0;
|
||||
|
||||
# Mapped path on the local filesystem
|
||||
mapped_path = "${config.services.sftpgo.dataDir}/${sharedFolderName}";
|
||||
|
||||
# All users in the matching group gain access
|
||||
users = config.users.groups.${sharedFolderName}.members;
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
# Generated Host Key for connecting to SFTPGo's sftp subsystem.
|
||||
snakeOilHostKey = pkgs.writeText "sftpgo_ed25519_host_key" ''
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACBOtQu6U135yxtrvUqPoozUymkjoNNPVK6rqjS936RLtQAAAJAXOMoSFzjK
|
||||
EgAAAAtzc2gtZWQyNTUxOQAAACBOtQu6U135yxtrvUqPoozUymkjoNNPVK6rqjS936RLtQ
|
||||
AAAEAoRLEV1VD80mg314ObySpfrCcUqtWoOSS3EtMPPhx08U61C7pTXfnLG2u9So+ijNTK
|
||||
aSOg009UrquqNL3fpEu1AAAADHNmdHBnb0BuaXhvcwE=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
'';
|
||||
|
||||
adminUsername = "admin";
|
||||
adminPassword = "secretadminpassword";
|
||||
aliceUsername = "alice";
|
||||
alicePassword = "secretalicepassword";
|
||||
bobUsername = "bob";
|
||||
bobPassword = "secretbobpassword";
|
||||
eveUsername = "eve";
|
||||
evePassword = "secretevepassword";
|
||||
sharedFolderName = "shared";
|
||||
|
||||
# A file for testing uploading via SFTP
|
||||
testFile = pkgs.writeText "test.txt" "hello world";
|
||||
sharedFile = pkgs.writeText "shared.txt" "shared content";
|
||||
|
||||
# Define the for exposing SFTP
|
||||
sftpPort = 2022;
|
||||
|
||||
# Define the for exposing HTTP
|
||||
httpPort = 8080;
|
||||
in
|
||||
{
|
||||
name = "sftpgo";
|
||||
|
||||
meta.maintainers = with maintainers; [ yayayayaka ];
|
||||
|
||||
nodes = {
|
||||
server = { nodes, ... }: {
|
||||
networking.firewall.allowedTCPPorts = [ sftpPort httpPort ];
|
||||
|
||||
# nodes.server.configure postgresql database
|
||||
services.postgresql = {
|
||||
enable = true;
|
||||
ensureDatabases = [ "sftpgo" ];
|
||||
ensureUsers = [{
|
||||
name = "sftpgo";
|
||||
ensurePermissions."DATABASE sftpgo" = "ALL PRIVILEGES";
|
||||
}];
|
||||
};
|
||||
|
||||
services.sftpgo = {
|
||||
enable = true;
|
||||
|
||||
loadDataFile = (loadDataJson nodes.server);
|
||||
|
||||
settings = {
|
||||
data_provider = {
|
||||
driver = "postgresql";
|
||||
name = "sftpgo";
|
||||
username = "sftpgo";
|
||||
host = "/run/postgresql";
|
||||
port = 5432;
|
||||
|
||||
# Enables the possibility to create an initial admin user on first startup.
|
||||
create_default_admin = true;
|
||||
};
|
||||
|
||||
httpd.bindings = [
|
||||
{
|
||||
address = ""; # listen on all interfaces
|
||||
port = httpPort;
|
||||
enable_https = false;
|
||||
|
||||
enable_web_client = true;
|
||||
enable_web_admin = true;
|
||||
}
|
||||
];
|
||||
|
||||
# Enable sftpd
|
||||
sftpd = {
|
||||
bindings = [{
|
||||
address = ""; # listen on all interfaces
|
||||
port = sftpPort;
|
||||
}];
|
||||
host_keys = [ snakeOilHostKey ];
|
||||
password_authentication = false;
|
||||
keyboard_interactive_authentication = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.sftpgo = {
|
||||
after = [ "postgresql.service"];
|
||||
environment = {
|
||||
# Update existing users
|
||||
SFTPGO_LOADDATA_MODE = "0";
|
||||
SFTPGO_DEFAULT_ADMIN_USERNAME = adminUsername;
|
||||
|
||||
# This will end up in cleartext in the systemd service.
|
||||
# Don't use this approach in production!
|
||||
SFTPGO_DEFAULT_ADMIN_PASSWORD = adminPassword;
|
||||
};
|
||||
};
|
||||
|
||||
# Sets up the folder hierarchy on the local filesystem
|
||||
systemd.tmpfiles.rules =
|
||||
let
|
||||
sftpgoUser = nodes.server.services.sftpgo.user;
|
||||
sftpgoGroup = nodes.server.services.sftpgo.group;
|
||||
statePath = nodes.server.services.sftpgo.dataDir;
|
||||
in [
|
||||
# Create state directory
|
||||
"d ${statePath} 0750 ${sftpgoUser} ${sftpgoGroup} -"
|
||||
"d ${statePath}/users 0750 ${sftpgoUser} ${sftpgoGroup} -"
|
||||
|
||||
# Created shared folder directories
|
||||
"d ${statePath}/${sharedFolderName} 2770 ${sftpgoUser} ${sharedFolderName} -"
|
||||
]
|
||||
++ mapAttrsToList (name: user:
|
||||
# Create private user directories
|
||||
''
|
||||
d ${statePath}/users/${user.name} 0700 ${sftpgoUser} ${sftpgoGroup} -
|
||||
d ${statePath}/users/${user.name}/private 0700 ${sftpgoUser} ${sftpgoGroup} -
|
||||
''
|
||||
) (normalUsers nodes.server);
|
||||
|
||||
users.users =
|
||||
let
|
||||
commonAttrs = {
|
||||
isNormalUser = true;
|
||||
openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
|
||||
};
|
||||
in {
|
||||
# SFTPGo admin user
|
||||
admin = commonAttrs // {
|
||||
password = adminPassword;
|
||||
};
|
||||
|
||||
# Alice and bob share folders with each other
|
||||
alice = commonAttrs // {
|
||||
password = alicePassword;
|
||||
extraGroups = [ sharedFolderName ];
|
||||
};
|
||||
|
||||
bob = commonAttrs // {
|
||||
password = bobPassword;
|
||||
extraGroups = [ sharedFolderName ];
|
||||
};
|
||||
|
||||
# Eve has no shared folders
|
||||
eve = commonAttrs // {
|
||||
password = evePassword;
|
||||
};
|
||||
};
|
||||
|
||||
users.groups.${sharedFolderName} = {};
|
||||
|
||||
specialisation = {
|
||||
# A specialisation for asserting that SFTPGo can bind to privileged ports.
|
||||
privilegedPorts.configuration = { ... }: {
|
||||
networking.firewall.allowedTCPPorts = [ 22 80 ];
|
||||
services.sftpgo = {
|
||||
settings = {
|
||||
sftpd.bindings = mkForce [{
|
||||
address = "";
|
||||
port = 22;
|
||||
}];
|
||||
|
||||
httpd.bindings = mkForce [{
|
||||
address = "";
|
||||
port = 80;
|
||||
}];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
client = { nodes, ... }: {
|
||||
# Add the SFTPGo host key to the global known_hosts file
|
||||
programs.ssh.knownHosts =
|
||||
let
|
||||
commonAttrs = {
|
||||
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE61C7pTXfnLG2u9So+ijNTKaSOg009UrquqNL3fpEu1";
|
||||
};
|
||||
in {
|
||||
"server" = commonAttrs;
|
||||
"[server]:2022" = commonAttrs;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = { nodes, ... }: let
|
||||
# A function to generate test cases for wheter
|
||||
# a specified username is expected to access the shared folder.
|
||||
accessSharedFoldersSubtest =
|
||||
{ # The username to run as
|
||||
username
|
||||
# Whether the tests are expected to succeed or not
|
||||
, shouldSucceed ? true
|
||||
}: ''
|
||||
with subtest("Test whether ${username} can access shared folders"):
|
||||
client.${if shouldSucceed then "succeed" else "fail"}("sftp -P ${toString sftpPort} -b ${
|
||||
pkgs.writeText "${username}-ls-${sharedFolderName}" ''
|
||||
ls ${sharedFolderName}
|
||||
''
|
||||
} ${username}@server")
|
||||
'';
|
||||
statePath = nodes.server.services.sftpgo.dataDir;
|
||||
in ''
|
||||
start_all()
|
||||
|
||||
client.wait_for_unit("default.target")
|
||||
server.wait_for_unit("sftpgo.service")
|
||||
|
||||
with subtest("web client"):
|
||||
client.wait_until_succeeds("curl -sSf http://server:${toString httpPort}/web/client/login")
|
||||
|
||||
# Ensure sftpgo found the static folder
|
||||
client.wait_until_succeeds("curl -o /dev/null -sSf http://server:${toString httpPort}/static/favicon.ico")
|
||||
|
||||
with subtest("Setup SSH keys"):
|
||||
client.succeed("mkdir -m 700 /root/.ssh")
|
||||
client.succeed("cat ${snakeOilPrivateKey} > /root/.ssh/id_ecdsa")
|
||||
client.succeed("chmod 600 /root/.ssh/id_ecdsa")
|
||||
|
||||
with subtest("Copy a file over sftp"):
|
||||
client.wait_until_succeeds("scp -P ${toString sftpPort} ${toString testFile} alice@server:/private/${testFile.name}")
|
||||
server.succeed("test -s ${statePath}/users/alice/private/${testFile.name}")
|
||||
|
||||
# The configured ACL should prevent uploading files to the root directory
|
||||
client.fail("scp -P ${toString sftpPort} ${toString testFile} alice@server:/")
|
||||
|
||||
with subtest("Attempting an interactive SSH sessions must fail"):
|
||||
client.fail("ssh -p ${toString sftpPort} alice@server")
|
||||
|
||||
${accessSharedFoldersSubtest {
|
||||
username = "alice";
|
||||
shouldSucceed = true;
|
||||
}}
|
||||
|
||||
${accessSharedFoldersSubtest {
|
||||
username = "bob";
|
||||
shouldSucceed = true;
|
||||
}}
|
||||
|
||||
${accessSharedFoldersSubtest {
|
||||
username = "eve";
|
||||
shouldSucceed = false;
|
||||
}}
|
||||
|
||||
with subtest("Test sharing files"):
|
||||
# Alice uploads a file to shared folder
|
||||
client.succeed("scp -P ${toString sftpPort} ${toString sharedFile} alice@server:/${sharedFolderName}/${sharedFile.name}")
|
||||
server.succeed("test -s ${statePath}/${sharedFolderName}/${sharedFile.name}")
|
||||
|
||||
# Bob downloads the file from shared folder
|
||||
client.succeed("scp -P ${toString sftpPort} bob@server:/shared/${sharedFile.name} ${sharedFile.name}")
|
||||
client.succeed("test -s ${sharedFile.name}")
|
||||
|
||||
# Eve should not get the file from shared folder
|
||||
client.fail("scp -P ${toString sftpPort} eve@server:/shared/${sharedFile.name}")
|
||||
|
||||
server.succeed("/run/current-system/specialisation/privilegedPorts/bin/switch-to-configuration test")
|
||||
|
||||
client.wait_until_succeeds("sftp -P 22 -b ${pkgs.writeText "get-hello-world.txt" ''
|
||||
get /private/${testFile.name}
|
||||
''} alice@server")
|
||||
'';
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
, buildGoModule
|
||||
, fetchFromGitHub
|
||||
, installShellFiles
|
||||
, nixosTests
|
||||
}:
|
||||
|
||||
buildGoModule rec {
|
||||
@ -44,6 +45,8 @@ buildGoModule rec {
|
||||
cp -r ./{openapi,static,templates} "$shareDirectory"
|
||||
'';
|
||||
|
||||
passthru.tests = nixosTests.sftpgo;
|
||||
|
||||
meta = with lib; {
|
||||
homepage = "https://github.com/drakkan/sftpgo";
|
||||
changelog = "https://github.com/drakkan/sftpgo/releases/tag/v${version}";
|
||||
|
Loading…
Reference in New Issue
Block a user