at master 9.0 kB view raw
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}