1{ system ? builtins.currentSystem
2, config ? { }
3, pkgs ? import ../.. { inherit system config; }
4}:
5
6# These tests will:
7# * Set up a bitwarden-rs server
8# * Have Firefox use the web vault to create an account, log in, and save a password to the valut
9# * Have the bw cli log in and read that password from the vault
10#
11# Note that Firefox must be on the same machine as the server for WebCrypto APIs to be available (or HTTPS must be configured)
12#
13# The same tests should work without modification on the official bitwarden server, if we ever package that.
14
15with import ../lib/testing-python.nix { inherit system pkgs; };
16with pkgs.lib;
17let
18 backends = [ "sqlite" "mysql" "postgresql" ];
19
20 dbPassword = "please_dont_hack";
21
22 userEmail = "meow@example.com";
23 userPassword = "also_super_secret_ZJWpBKZi668QGt"; # Must be complex to avoid interstitial warning on the signup page
24
25 storedPassword = "seeeecret";
26
27 makeBitwardenTest = backend: makeTest {
28 name = "bitwarden_rs-${backend}";
29 meta = {
30 maintainers = with pkgs.lib.maintainers; [ jjjollyjim ];
31 };
32
33 nodes = {
34 server = { pkgs, ... }:
35 let backendConfig = {
36 mysql = {
37 services.mysql = {
38 enable = true;
39 initialScript = pkgs.writeText "mysql-init.sql" ''
40 CREATE DATABASE bitwarden;
41 CREATE USER 'bitwardenuser'@'localhost' IDENTIFIED BY '${dbPassword}';
42 GRANT ALL ON `bitwarden`.* TO 'bitwardenuser'@'localhost';
43 FLUSH PRIVILEGES;
44 '';
45 package = pkgs.mysql;
46 };
47
48 services.bitwarden_rs.config.databaseUrl = "mysql://bitwardenuser:${dbPassword}@localhost/bitwarden";
49
50 systemd.services.bitwarden_rs.after = [ "mysql.service" ];
51 };
52
53 postgresql = {
54 services.postgresql = {
55 enable = true;
56 initialScript = pkgs.writeText "postgresql-init.sql" ''
57 CREATE DATABASE bitwarden;
58 CREATE USER bitwardenuser WITH PASSWORD '${dbPassword}';
59 GRANT ALL PRIVILEGES ON DATABASE bitwarden TO bitwardenuser;
60 '';
61 };
62
63 services.bitwarden_rs.config.databaseUrl = "postgresql://bitwardenuser:${dbPassword}@localhost/bitwarden";
64
65 systemd.services.bitwarden_rs.after = [ "postgresql.service" ];
66 };
67
68 sqlite = { };
69 };
70 in
71 mkMerge [
72 backendConfig.${backend}
73 {
74 services.bitwarden_rs = {
75 enable = true;
76 dbBackend = backend;
77 config.rocketPort = 80;
78 };
79
80 networking.firewall.allowedTCPPorts = [ 80 ];
81
82 environment.systemPackages =
83 let
84 testRunner = pkgs.writers.writePython3Bin "test-runner"
85 {
86 libraries = [ pkgs.python3Packages.selenium ];
87 } ''
88 from selenium.webdriver import Firefox
89 from selenium.webdriver.firefox.options import Options
90 from selenium.webdriver.support.ui import WebDriverWait
91 from selenium.webdriver.support import expected_conditions as EC
92
93 options = Options()
94 options.add_argument('--headless')
95 driver = Firefox(options=options)
96
97 driver.implicitly_wait(20)
98 driver.get('http://localhost/#/register')
99
100 wait = WebDriverWait(driver, 10)
101
102 wait.until(EC.title_contains("Create Account"))
103
104 driver.find_element_by_css_selector('input#email').send_keys(
105 '${userEmail}'
106 )
107 driver.find_element_by_css_selector('input#name').send_keys(
108 'A Cat'
109 )
110 driver.find_element_by_css_selector('input#masterPassword').send_keys(
111 '${userPassword}'
112 )
113 driver.find_element_by_css_selector('input#masterPasswordRetype').send_keys(
114 '${userPassword}'
115 )
116 driver.find_element_by_css_selector('input#acceptPolicies').click()
117
118 driver.find_element_by_xpath("//button[contains(., 'Submit')]").click()
119
120 wait.until_not(EC.title_contains("Create Account"))
121
122 driver.find_element_by_css_selector('input#masterPassword').send_keys(
123 '${userPassword}'
124 )
125 driver.find_element_by_xpath("//button[contains(., 'Log In')]").click()
126
127 wait.until(EC.title_contains("My Vault"))
128
129 driver.find_element_by_xpath("//button[contains(., 'Add Item')]").click()
130
131 driver.find_element_by_css_selector('input#name').send_keys(
132 'secrets'
133 )
134 driver.find_element_by_css_selector('input#loginPassword').send_keys(
135 '${storedPassword}'
136 )
137
138 driver.find_element_by_xpath("//button[contains(., 'Save')]").click()
139 '';
140 in
141 [ pkgs.firefox-unwrapped pkgs.geckodriver testRunner ];
142
143 virtualisation.memorySize = 768;
144 }
145 ];
146
147 client = { pkgs, ... }:
148 {
149 environment.systemPackages = [ pkgs.bitwarden-cli ];
150 };
151 };
152
153 testScript = ''
154 start_all()
155 server.wait_for_unit("bitwarden_rs.service")
156 server.wait_for_open_port(80)
157
158 with subtest("configure the cli"):
159 client.succeed("bw --nointeraction config server http://server")
160
161 with subtest("can't login to nonexistant account"):
162 client.fail(
163 "bw --nointeraction --raw login ${userEmail} ${userPassword}"
164 )
165
166 with subtest("use the web interface to sign up, log in, and save a password"):
167 server.succeed("PYTHONUNBUFFERED=1 test-runner | systemd-cat -t test-runner")
168
169 with subtest("log in with the cli"):
170 key = client.succeed(
171 "bw --nointeraction --raw login ${userEmail} ${userPassword}"
172 ).strip()
173
174 with subtest("sync with the cli"):
175 client.succeed(f"bw --nointeraction --raw --session {key} sync -f")
176
177 with subtest("get the password with the cli"):
178 password = client.succeed(
179 f"bw --nointeraction --raw --session {key} list items | ${pkgs.jq}/bin/jq -r .[].login.password"
180 )
181 assert password.strip() == "${storedPassword}"
182 '';
183 };
184in
185builtins.listToAttrs (
186 map
187 (backend: { name = backend; value = makeBitwardenTest backend; })
188 backends
189)