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