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 from selenium.common.exceptions import ElementClickInterceptedException
40
41
42 def click_when_unobstructed(mark):
43 while True:
44 try:
45 wait.until(EC.element_to_be_clickable(mark)).click()
46 break
47 except ElementClickInterceptedException:
48 continue
49
50
51 options = Options()
52 options.add_argument('--headless')
53 driver = Firefox(options=options)
54
55 driver.implicitly_wait(20)
56 driver.get('http://localhost:8080/#/signup')
57
58 wait = WebDriverWait(driver, 10)
59
60 wait.until(EC.title_contains("Vaultwarden Web"))
61
62 driver.find_element(By.CSS_SELECTOR, 'input#register-start_form_input_email').send_keys(
63 '${userEmail}'
64 )
65 driver.find_element(By.CSS_SELECTOR, 'input#register-start_form_input_name').send_keys(
66 'A Cat'
67 )
68 driver.find_element(By.XPATH, "//button[contains(., 'Continue')]").click()
69 driver.find_element(By.CSS_SELECTOR, 'input#input-password-form_new-password').send_keys(
70 '${userPassword}'
71 )
72 driver.find_element(By.CSS_SELECTOR, 'input#input-password-form_new-password-confirm').send_keys(
73 '${userPassword}'
74 )
75 if driver.find_element(By.XPATH, '//input[@formcontrolname="checkForBreaches"]').is_selected():
76 driver.find_element(By.XPATH, '//input[@formcontrolname="checkForBreaches"]').click()
77
78 driver.find_element(By.XPATH, "//button[contains(., 'Create account')]").click()
79
80 wait.until_not(EC.title_contains("Set a strong password"))
81
82 click_when_unobstructed((By.XPATH, "//button[contains(., 'New item')]"))
83
84 driver.find_element(By.XPATH, '//input[@formcontrolname="name"]').send_keys(
85 'secrets'
86 )
87 driver.find_element(By.XPATH, '//input[@formcontrolname="password"]').send_keys(
88 '${storedPassword}'
89 )
90
91 driver.find_element(By.XPATH, "//button[contains(., 'Save')]").click()
92 '';
93 in
94 {
95 inherit name;
96
97 meta = {
98 maintainers = with pkgs.lib.maintainers; [
99 dotlambda
100 SuperSandro2000
101 ];
102 };
103
104 nodes = {
105 server =
106 { pkgs, ... }:
107 lib.mkMerge [
108 {
109 mysql = {
110 services.mysql = {
111 enable = true;
112 initialScript = pkgs.writeText "mysql-init.sql" ''
113 CREATE DATABASE bitwarden;
114 CREATE USER 'bitwardenuser'@'localhost' IDENTIFIED BY '${dbPassword}';
115 GRANT ALL ON `bitwarden`.* TO 'bitwardenuser'@'localhost';
116 FLUSH PRIVILEGES;
117 '';
118 package = pkgs.mariadb;
119 };
120
121 services.vaultwarden.config.databaseUrl = "mysql://bitwardenuser:${dbPassword}@localhost/bitwarden";
122
123 systemd.services.vaultwarden.after = [ "mysql.service" ];
124 };
125
126 postgresql = {
127 services.postgresql = {
128 enable = true;
129 ensureDatabases = [ "vaultwarden" ];
130 ensureUsers = [
131 {
132 name = "vaultwarden";
133 ensureDBOwnership = true;
134 }
135 ];
136 };
137
138 services.vaultwarden.config.databaseUrl = "postgresql:///vaultwarden?host=/run/postgresql";
139
140 systemd.services.vaultwarden.after = [ "postgresql.target" ];
141 };
142
143 sqlite = {
144 services.vaultwarden.backupDir = "/srv/backups/vaultwarden";
145
146 environment.systemPackages = [ pkgs.sqlite ];
147 };
148 }
149 .${backend}
150
151 {
152 services.vaultwarden = {
153 enable = true;
154 dbBackend = backend;
155 config = {
156 rocketAddress = "::";
157 rocketPort = 8080;
158 };
159 };
160
161 networking.firewall.allowedTCPPorts = [ 8080 ];
162
163 environment.systemPackages = [
164 pkgs.firefox-unwrapped
165 pkgs.geckodriver
166 testRunner
167 ];
168 }
169 ];
170 }
171 // lib.optionalAttrs withClient {
172 client =
173 { pkgs, ... }:
174 {
175 environment.systemPackages = [ pkgs.bitwarden-cli ];
176 };
177 };
178
179 testScript =
180 if testScript != null then
181 testScript
182 else
183 ''
184 import json
185
186 start_all()
187 server.wait_for_unit("vaultwarden.service")
188 server.wait_for_open_port(8080)
189
190 with subtest("configure the cli"):
191 client.succeed("bw --nointeraction config server http://server:8080")
192
193 with subtest("can't login to nonexistent account"):
194 client.fail(
195 "bw --nointeraction --raw login ${userEmail} ${userPassword}"
196 )
197
198 with subtest("use the web interface to sign up, log in, and save a password"):
199 server.succeed("PYTHONUNBUFFERED=1 systemd-cat -t test-runner test-runner")
200
201 with subtest("log in with the cli"):
202 key = client.succeed(
203 "bw --nointeraction --raw login ${userEmail} ${userPassword}"
204 ).strip()
205
206 with subtest("sync with the cli"):
207 client.succeed(f"bw --nointeraction --raw --session {key} sync -f")
208
209 with subtest("get the password with the cli"):
210 output = json.loads(client.succeed(f"bw --nointeraction --raw --session {key} list items"))
211
212 assert output[0]['login']['password'] == "${storedPassword}"
213
214 with subtest("Check systemd unit hardening"):
215 server.log(server.succeed("systemd-analyze security vaultwarden.service | grep -v ✓"))
216 '';
217 }
218 );
219in
220builtins.mapAttrs (k: v: makeVaultwardenTest k v) {
221 mysql = { };
222 postgresql = { };
223 sqlite = { };
224 sqlite-backup = {
225 backend = "sqlite";
226 withClient = false;
227
228 testScript = ''
229 start_all()
230 server.wait_for_unit("vaultwarden.service")
231 server.wait_for_open_port(8080)
232
233 with subtest("Set up vaultwarden"):
234 server.succeed("PYTHONUNBUFFERED=1 test-runner | systemd-cat -t test-runner")
235
236 with subtest("Run the backup script"):
237 server.start_job("backup-vaultwarden.service")
238
239 with subtest("Check that backup exists"):
240 server.succeed('[ -d "/srv/backups/vaultwarden" ]')
241 server.succeed('[ -f "/srv/backups/vaultwarden/db.sqlite3" ]')
242 server.succeed('[ -d "/srv/backups/vaultwarden/attachments" ]')
243 server.succeed('[ -f "/srv/backups/vaultwarden/rsa_key.pem" ]')
244 # Ensure only the db backed up with the backup command exists and not the other db files.
245 server.succeed('[ ! -f "/srv/backups/vaultwarden/db.sqlite3-shm" ]')
246 '';
247 };
248}