nixpkgs mirror (for testing) github.com/NixOS/nixpkgs
nix
at netboot-syslinux-multiplatform 384 lines 13 kB view raw
1# SFTPGo NixOS test 2# 3# This NixOS test sets up a basic test scenario for the SFTPGo module 4# and covers the following scenarios: 5# - uploading a file via sftp 6# - downloading the file over sftp 7# - assert that the ACLs are respected 8# - share a file between alice and bob (using sftp) 9# - assert that eve cannot acceess the shared folder between alice and bob. 10# 11# Additional test coverage for the remaining protocols (i.e. ftp, http and webdav) 12# would be a nice to have for the future. 13{ pkgs, lib, ... }: 14 15with lib; 16 17let 18 inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey; 19 20 # Returns an attributeset of users who are not system users. 21 normalUsers = config: 22 filterAttrs (name: user: user.isNormalUser) config.users.users; 23 24 # Returns true if a user is a member of the given group 25 isMemberOf = 26 config: 27 # str 28 groupName: 29 # users.users attrset 30 user: 31 any (x: x == user.name) config.users.groups.${groupName}.members; 32 33 # Generates a valid SFTPGo user configuration for a given user 34 # Will be converted to JSON and loaded on application startup. 35 generateUserAttrSet = 36 config: 37 # attrset returned by config.users.users.<username> 38 user: { 39 # 0: user is disabled, login is not allowed 40 # 1: user is enabled 41 status = 1; 42 43 username = user.name; 44 password = ""; # disables password authentication 45 public_keys = user.openssh.authorizedKeys.keys; 46 email = "${user.name}@example.com"; 47 48 # User home directory on the local filesystem 49 home_dir = "${config.services.sftpgo.dataDir}/users/${user.name}"; 50 51 # Defines a mapping between virtual SFTPGo paths and filesystem paths outside the user home directory. 52 # 53 # Supported for local filesystem only. If one or more of the specified folders are not 54 # inside the dataprovider they will be automatically created. 55 # You have to create the folder on the filesystem yourself 56 virtual_folders = 57 optional (isMemberOf config sharedFolderName user) { 58 name = sharedFolderName; 59 mapped_path = "${config.services.sftpgo.dataDir}/${sharedFolderName}"; 60 virtual_path = "/${sharedFolderName}"; 61 }; 62 63 # Defines the ACL on the virtual filesystem 64 permissions = 65 recursiveUpdate { 66 "/" = [ "list" ]; # read-only top level directory 67 "/private" = [ "*" ]; # private subdirectory, not shared with others 68 } (optionalAttrs (isMemberOf config "shared" user) { 69 "/shared" = [ "*" ]; 70 }); 71 72 filters = { 73 allowed_ip = []; 74 denied_ip = []; 75 web_client = [ 76 "password-change-disabled" 77 "password-reset-disabled" 78 "api-key-auth-change-disabled" 79 ]; 80 }; 81 82 upload_bandwidth = 0; # unlimited 83 download_bandwidth = 0; # unlimited 84 expiration_date = 0; # means no expiration 85 max_sessions = 0; 86 quota_size = 0; 87 quota_files = 0; 88 }; 89 90 # Generates a json file containing a static configuration 91 # of users and folders to import to SFTPGo. 92 loadDataJson = config: pkgs.writeText "users-and-folders.json" (builtins.toJSON { 93 users = 94 mapAttrsToList (name: user: generateUserAttrSet config user) (normalUsers config); 95 96 folders = [ 97 { 98 name = sharedFolderName; 99 description = "shared folder"; 100 101 # 0: local filesystem 102 # 1: AWS S3 compatible 103 # 2: Google Cloud Storage 104 filesystem.provider = 0; 105 106 # Mapped path on the local filesystem 107 mapped_path = "${config.services.sftpgo.dataDir}/${sharedFolderName}"; 108 109 # All users in the matching group gain access 110 users = config.users.groups.${sharedFolderName}.members; 111 } 112 ]; 113 }); 114 115 # Generated Host Key for connecting to SFTPGo's sftp subsystem. 116 snakeOilHostKey = pkgs.writeText "sftpgo_ed25519_host_key" '' 117 -----BEGIN OPENSSH PRIVATE KEY----- 118 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 119 QyNTUxOQAAACBOtQu6U135yxtrvUqPoozUymkjoNNPVK6rqjS936RLtQAAAJAXOMoSFzjK 120 EgAAAAtzc2gtZWQyNTUxOQAAACBOtQu6U135yxtrvUqPoozUymkjoNNPVK6rqjS936RLtQ 121 AAAEAoRLEV1VD80mg314ObySpfrCcUqtWoOSS3EtMPPhx08U61C7pTXfnLG2u9So+ijNTK 122 aSOg009UrquqNL3fpEu1AAAADHNmdHBnb0BuaXhvcwE= 123 -----END OPENSSH PRIVATE KEY----- 124 ''; 125 126 adminUsername = "admin"; 127 adminPassword = "secretadminpassword"; 128 aliceUsername = "alice"; 129 alicePassword = "secretalicepassword"; 130 bobUsername = "bob"; 131 bobPassword = "secretbobpassword"; 132 eveUsername = "eve"; 133 evePassword = "secretevepassword"; 134 sharedFolderName = "shared"; 135 136 # A file for testing uploading via SFTP 137 testFile = pkgs.writeText "test.txt" "hello world"; 138 sharedFile = pkgs.writeText "shared.txt" "shared content"; 139 140 # Define the for exposing SFTP 141 sftpPort = 2022; 142 143 # Define the for exposing HTTP 144 httpPort = 8080; 145in 146{ 147 name = "sftpgo"; 148 149 meta.maintainers = with maintainers; [ yayayayaka ]; 150 151 nodes = { 152 server = { nodes, ... }: { 153 networking.firewall.allowedTCPPorts = [ sftpPort httpPort ]; 154 155 # nodes.server.configure postgresql database 156 services.postgresql = { 157 enable = true; 158 ensureDatabases = [ "sftpgo" ]; 159 ensureUsers = [{ 160 name = "sftpgo"; 161 ensurePermissions."DATABASE sftpgo" = "ALL PRIVILEGES"; 162 }]; 163 }; 164 165 services.sftpgo = { 166 enable = true; 167 168 loadDataFile = (loadDataJson nodes.server); 169 170 settings = { 171 data_provider = { 172 driver = "postgresql"; 173 name = "sftpgo"; 174 username = "sftpgo"; 175 host = "/run/postgresql"; 176 port = 5432; 177 178 # Enables the possibility to create an initial admin user on first startup. 179 create_default_admin = true; 180 }; 181 182 httpd.bindings = [ 183 { 184 address = ""; # listen on all interfaces 185 port = httpPort; 186 enable_https = false; 187 188 enable_web_client = true; 189 enable_web_admin = true; 190 } 191 ]; 192 193 # Enable sftpd 194 sftpd = { 195 bindings = [{ 196 address = ""; # listen on all interfaces 197 port = sftpPort; 198 }]; 199 host_keys = [ snakeOilHostKey ]; 200 password_authentication = false; 201 keyboard_interactive_authentication = false; 202 }; 203 }; 204 }; 205 206 systemd.services.sftpgo = { 207 after = [ "postgresql.service"]; 208 environment = { 209 # Update existing users 210 SFTPGO_LOADDATA_MODE = "0"; 211 SFTPGO_DEFAULT_ADMIN_USERNAME = adminUsername; 212 213 # This will end up in cleartext in the systemd service. 214 # Don't use this approach in production! 215 SFTPGO_DEFAULT_ADMIN_PASSWORD = adminPassword; 216 }; 217 }; 218 219 # Sets up the folder hierarchy on the local filesystem 220 systemd.tmpfiles.rules = 221 let 222 sftpgoUser = nodes.server.services.sftpgo.user; 223 sftpgoGroup = nodes.server.services.sftpgo.group; 224 statePath = nodes.server.services.sftpgo.dataDir; 225 in [ 226 # Create state directory 227 "d ${statePath} 0750 ${sftpgoUser} ${sftpgoGroup} -" 228 "d ${statePath}/users 0750 ${sftpgoUser} ${sftpgoGroup} -" 229 230 # Created shared folder directories 231 "d ${statePath}/${sharedFolderName} 2770 ${sftpgoUser} ${sharedFolderName} -" 232 ] 233 ++ mapAttrsToList (name: user: 234 # Create private user directories 235 '' 236 d ${statePath}/users/${user.name} 0700 ${sftpgoUser} ${sftpgoGroup} - 237 d ${statePath}/users/${user.name}/private 0700 ${sftpgoUser} ${sftpgoGroup} - 238 '' 239 ) (normalUsers nodes.server); 240 241 users.users = 242 let 243 commonAttrs = { 244 isNormalUser = true; 245 openssh.authorizedKeys.keys = [ snakeOilPublicKey ]; 246 }; 247 in { 248 # SFTPGo admin user 249 admin = commonAttrs // { 250 password = adminPassword; 251 }; 252 253 # Alice and bob share folders with each other 254 alice = commonAttrs // { 255 password = alicePassword; 256 extraGroups = [ sharedFolderName ]; 257 }; 258 259 bob = commonAttrs // { 260 password = bobPassword; 261 extraGroups = [ sharedFolderName ]; 262 }; 263 264 # Eve has no shared folders 265 eve = commonAttrs // { 266 password = evePassword; 267 }; 268 }; 269 270 users.groups.${sharedFolderName} = {}; 271 272 specialisation = { 273 # A specialisation for asserting that SFTPGo can bind to privileged ports. 274 privilegedPorts.configuration = { ... }: { 275 networking.firewall.allowedTCPPorts = [ 22 80 ]; 276 services.sftpgo = { 277 settings = { 278 sftpd.bindings = mkForce [{ 279 address = ""; 280 port = 22; 281 }]; 282 283 httpd.bindings = mkForce [{ 284 address = ""; 285 port = 80; 286 }]; 287 }; 288 }; 289 }; 290 }; 291 }; 292 293 client = { nodes, ... }: { 294 # Add the SFTPGo host key to the global known_hosts file 295 programs.ssh.knownHosts = 296 let 297 commonAttrs = { 298 publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE61C7pTXfnLG2u9So+ijNTKaSOg009UrquqNL3fpEu1"; 299 }; 300 in { 301 "server" = commonAttrs; 302 "[server]:2022" = commonAttrs; 303 }; 304 }; 305 }; 306 307 testScript = { nodes, ... }: let 308 # A function to generate test cases for wheter 309 # a specified username is expected to access the shared folder. 310 accessSharedFoldersSubtest = 311 { # The username to run as 312 username 313 # Whether the tests are expected to succeed or not 314 , shouldSucceed ? true 315 }: '' 316 with subtest("Test whether ${username} can access shared folders"): 317 client.${if shouldSucceed then "succeed" else "fail"}("sftp -P ${toString sftpPort} -b ${ 318 pkgs.writeText "${username}-ls-${sharedFolderName}" '' 319 ls ${sharedFolderName} 320 '' 321 } ${username}@server") 322 ''; 323 statePath = nodes.server.services.sftpgo.dataDir; 324 in '' 325 start_all() 326 327 client.wait_for_unit("default.target") 328 server.wait_for_unit("sftpgo.service") 329 330 with subtest("web client"): 331 client.wait_until_succeeds("curl -sSf http://server:${toString httpPort}/web/client/login") 332 333 # Ensure sftpgo found the static folder 334 client.wait_until_succeeds("curl -o /dev/null -sSf http://server:${toString httpPort}/static/favicon.ico") 335 336 with subtest("Setup SSH keys"): 337 client.succeed("mkdir -m 700 /root/.ssh") 338 client.succeed("cat ${snakeOilPrivateKey} > /root/.ssh/id_ecdsa") 339 client.succeed("chmod 600 /root/.ssh/id_ecdsa") 340 341 with subtest("Copy a file over sftp"): 342 client.wait_until_succeeds("scp -P ${toString sftpPort} ${toString testFile} alice@server:/private/${testFile.name}") 343 server.succeed("test -s ${statePath}/users/alice/private/${testFile.name}") 344 345 # The configured ACL should prevent uploading files to the root directory 346 client.fail("scp -P ${toString sftpPort} ${toString testFile} alice@server:/") 347 348 with subtest("Attempting an interactive SSH sessions must fail"): 349 client.fail("ssh -p ${toString sftpPort} alice@server") 350 351 ${accessSharedFoldersSubtest { 352 username = "alice"; 353 shouldSucceed = true; 354 }} 355 356 ${accessSharedFoldersSubtest { 357 username = "bob"; 358 shouldSucceed = true; 359 }} 360 361 ${accessSharedFoldersSubtest { 362 username = "eve"; 363 shouldSucceed = false; 364 }} 365 366 with subtest("Test sharing files"): 367 # Alice uploads a file to shared folder 368 client.succeed("scp -P ${toString sftpPort} ${toString sharedFile} alice@server:/${sharedFolderName}/${sharedFile.name}") 369 server.succeed("test -s ${statePath}/${sharedFolderName}/${sharedFile.name}") 370 371 # Bob downloads the file from shared folder 372 client.succeed("scp -P ${toString sftpPort} bob@server:/shared/${sharedFile.name} ${sharedFile.name}") 373 client.succeed("test -s ${sharedFile.name}") 374 375 # Eve should not get the file from shared folder 376 client.fail("scp -P ${toString sftpPort} eve@server:/shared/${sharedFile.name}") 377 378 server.succeed("/run/current-system/specialisation/privilegedPorts/bin/switch-to-configuration test") 379 380 client.wait_until_succeeds("sftp -P 22 -b ${pkgs.writeText "get-hello-world.txt" '' 381 get /private/${testFile.name} 382 ''} alice@server") 383 ''; 384}