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