1# These tests will:
2# * Set up a vaultwarden server
3# * Have Firefox use the web vault to create an account, log in, and save a password to the vault
4# * Have the bw cli log in and read that password from the vault
5#
6# Note that Firefox must be on the same machine as the server for WebCrypto APIs to be available (or HTTPS must be configured)
7#
8# The same tests should work without modification on the official bitwarden server, if we ever package that.
9
10let
11 makeVaultwardenTest =
12 name:
13 {
14 backend ? name,
15 withClient ? true,
16 testScript ? null,
17 }:
18 import ./make-test-python.nix (
19 { lib, pkgs, ... }:
20 let
21 dbPassword = "please_dont_hack";
22 userEmail = "meow@example.com";
23 userPassword = "also_super_secret_ZJWpBKZi668QGt"; # Must be complex to avoid interstitial warning on the signup page
24 storedPassword = "seeeecret";
25
26 testRunner =
27 pkgs.writers.writePython3Bin "test-runner"
28 {
29 libraries = [ pkgs.python3Packages.selenium ];
30 flakeIgnore = [ "E501" ];
31 }
32 ''
33
34 from selenium.webdriver.common.by import By
35 from selenium.webdriver import Firefox
36 from selenium.webdriver.firefox.options import Options
37 from selenium.webdriver.support.ui import WebDriverWait
38 from selenium.webdriver.support import expected_conditions as EC
39
40 options = Options()
41 options.add_argument('--headless')
42 driver = Firefox(options=options)
43
44 driver.implicitly_wait(20)
45 driver.get('http://localhost:8080/#/register')
46
47 wait = WebDriverWait(driver, 10)
48
49 wait.until(EC.title_contains("Vaultwarden Web"))
50
51 driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_email').send_keys(
52 '${userEmail}'
53 )
54 driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_name').send_keys(
55 'A Cat'
56 )
57 driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_master-password').send_keys(
58 '${userPassword}'
59 )
60 driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_confirm-master-password').send_keys(
61 '${userPassword}'
62 )
63 if driver.find_element(By.CSS_SELECTOR, 'input#checkForBreaches').is_selected():
64 driver.find_element(By.CSS_SELECTOR, 'input#checkForBreaches').click()
65
66 driver.find_element(By.XPATH, "//button[contains(., 'Create account')]").click()
67
68 wait.until_not(EC.title_contains("Create account"))
69
70 driver.find_element(By.XPATH, "//button[contains(., 'Continue')]").click()
71
72 driver.find_element(By.XPATH, '//input[@type="password"]').send_keys(
73 '${userPassword}'
74 )
75 driver.find_element(By.XPATH, "//button[contains(., 'Log in with master password')]").click()
76
77 wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'button#newItemDropdown'))).click()
78 driver.find_element(By.XPATH, "//button[contains(., 'Item')]").click()
79
80 driver.find_element(By.CSS_SELECTOR, 'input#name').send_keys(
81 'secrets'
82 )
83 driver.find_element(By.CSS_SELECTOR, 'input#loginPassword').send_keys(
84 '${storedPassword}'
85 )
86
87 driver.find_element(By.XPATH, "//button[contains(., 'Save')]").click()
88 '';
89 in
90 {
91 inherit name;
92
93 meta = {
94 maintainers = with pkgs.lib.maintainers; [
95 dotlambda
96 SuperSandro2000
97 ];
98 };
99
100 nodes =
101 {
102 server =
103 { pkgs, ... }:
104 lib.mkMerge [
105 {
106 mysql = {
107 services.mysql = {
108 enable = true;
109 initialScript = pkgs.writeText "mysql-init.sql" ''
110 CREATE DATABASE bitwarden;
111 CREATE USER 'bitwardenuser'@'localhost' IDENTIFIED BY '${dbPassword}';
112 GRANT ALL ON `bitwarden`.* TO 'bitwardenuser'@'localhost';
113 FLUSH PRIVILEGES;
114 '';
115 package = pkgs.mariadb;
116 };
117
118 services.vaultwarden.config.databaseUrl = "mysql://bitwardenuser:${dbPassword}@localhost/bitwarden";
119
120 systemd.services.vaultwarden.after = [ "mysql.service" ];
121 };
122
123 postgresql = {
124 services.postgresql = {
125 enable = true;
126 ensureDatabases = [ "vaultwarden" ];
127 ensureUsers = [
128 {
129 name = "vaultwarden";
130 ensureDBOwnership = true;
131 }
132 ];
133 };
134
135 services.vaultwarden.config.databaseUrl = "postgresql:///vaultwarden?host=/run/postgresql";
136
137 systemd.services.vaultwarden.after = [ "postgresql.service" ];
138 };
139
140 sqlite = {
141 services.vaultwarden.backupDir = "/srv/backups/vaultwarden";
142
143 environment.systemPackages = [ pkgs.sqlite ];
144 };
145 }
146 .${backend}
147
148 {
149 services.vaultwarden = {
150 enable = true;
151 dbBackend = backend;
152 config = {
153 rocketAddress = "::";
154 rocketPort = 8080;
155 };
156 };
157
158 networking.firewall.allowedTCPPorts = [ 8080 ];
159
160 environment.systemPackages = [
161 pkgs.firefox-unwrapped
162 pkgs.geckodriver
163 testRunner
164 ];
165 }
166 ];
167 }
168 // lib.optionalAttrs withClient {
169 client =
170 { pkgs, ... }:
171 {
172 environment.systemPackages = [ pkgs.bitwarden-cli ];
173 };
174 };
175
176 testScript =
177 if testScript != null then
178 testScript
179 else
180 ''
181 start_all()
182 server.wait_for_unit("vaultwarden.service")
183 server.wait_for_open_port(8080)
184
185 with subtest("configure the cli"):
186 client.succeed("bw --nointeraction config server http://server:8080")
187
188 with subtest("can't login to nonexistent account"):
189 client.fail(
190 "bw --nointeraction --raw login ${userEmail} ${userPassword}"
191 )
192
193 with subtest("use the web interface to sign up, log in, and save a password"):
194 server.succeed("PYTHONUNBUFFERED=1 systemd-cat -t test-runner test-runner")
195
196 with subtest("log in with the cli"):
197 key = client.succeed(
198 "bw --nointeraction --raw login ${userEmail} ${userPassword}"
199 ).strip()
200
201 with subtest("sync with the cli"):
202 client.succeed(f"bw --nointeraction --raw --session {key} sync -f")
203
204 with subtest("get the password with the cli"):
205 password = client.wait_until_succeeds(
206 f"bw --nointeraction --raw --session {key} list items | ${pkgs.jq}/bin/jq -r .[].login.password",
207 timeout=60
208 )
209 assert password.strip() == "${storedPassword}"
210
211 with subtest("Check systemd unit hardening"):
212 server.log(server.succeed("systemd-analyze security vaultwarden.service | grep -v ✓"))
213 '';
214 }
215 );
216in
217builtins.mapAttrs (k: v: makeVaultwardenTest k v) {
218 mysql = { };
219 postgresql = { };
220 sqlite = { };
221 sqlite-backup = {
222 backend = "sqlite";
223 withClient = false;
224
225 testScript = ''
226 start_all()
227 server.wait_for_unit("vaultwarden.service")
228 server.wait_for_open_port(8080)
229
230 with subtest("Set up vaultwarden"):
231 server.succeed("PYTHONUNBUFFERED=1 test-runner | systemd-cat -t test-runner")
232
233 with subtest("Run the backup script"):
234 server.start_job("backup-vaultwarden.service")
235
236 with subtest("Check that backup exists"):
237 server.succeed('[ -d "/srv/backups/vaultwarden" ]')
238 server.succeed('[ -f "/srv/backups/vaultwarden/db.sqlite3" ]')
239 server.succeed('[ -d "/srv/backups/vaultwarden/attachments" ]')
240 server.succeed('[ -f "/srv/backups/vaultwarden/rsa_key.pem" ]')
241 # Ensure only the db backed up with the backup command exists and not the other db files.
242 server.succeed('[ ! -f "/srv/backups/vaultwarden/db.sqlite3-shm" ]')
243 '';
244 };
245}