Merge pull request #292857 from SuperSandro2000/vaultwarden-backup-test

nixos/vaultwarden: add test for backup script
This commit is contained in:
Sandro 2024-06-15 23:47:10 +02:00 committed by GitHub
commit aa6f59e07e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 135 additions and 123 deletions

View File

@ -1019,7 +1019,7 @@ in {
vault-agent = handleTest ./vault-agent.nix {};
vault-dev = handleTest ./vault-dev.nix {};
vault-postgresql = handleTest ./vault-postgresql.nix {};
vaultwarden = handleTest ./vaultwarden.nix {};
vaultwarden = discoverTests (import ./vaultwarden.nix);
vector = handleTest ./vector {};
vengi-tools = handleTest ./vengi-tools.nix {};
victoriametrics = handleTest ./victoriametrics.nix {};

View File

@ -1,38 +1,94 @@
{ system ? builtins.currentSystem
, config ? { }
, pkgs ? import ../.. { inherit system config; }
}:
# These tests will:
# * Set up a vaultwarden server
# * Have Firefox use the web vault to create an account, log in, and save a password to the valut
# * Have Firefox use the web vault to create an account, log in, and save a password to the vault
# * Have the bw cli log in and read that password from the vault
#
# Note that Firefox must be on the same machine as the server for WebCrypto APIs to be available (or HTTPS must be configured)
#
# The same tests should work without modification on the official bitwarden server, if we ever package that.
with import ../lib/testing-python.nix { inherit system pkgs; };
with pkgs.lib;
let
backends = [ "sqlite" "mysql" "postgresql" ];
makeVaultwardenTest = name: {
backend ? name,
withClient ? true,
testScript ? null,
}: import ./make-test-python.nix ({ lib, pkgs, ...}: let
dbPassword = "please_dont_hack";
userEmail = "meow@example.com";
userPassword = "also_super_secret_ZJWpBKZi668QGt"; # Must be complex to avoid interstitial warning on the signup page
storedPassword = "seeeecret";
dbPassword = "please_dont_hack";
testRunner = pkgs.writers.writePython3Bin "test-runner" {
libraries = [ pkgs.python3Packages.selenium ];
flakeIgnore = [ "E501" ];
} ''
userEmail = "meow@example.com";
userPassword = "also_super_secret_ZJWpBKZi668QGt"; # Must be complex to avoid interstitial warning on the signup page
from selenium.webdriver.common.by import By
from selenium.webdriver import Firefox
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
storedPassword = "seeeecret";
options = Options()
options.add_argument('--headless')
driver = Firefox(options=options)
driver.implicitly_wait(20)
driver.get('http://localhost/#/register')
wait = WebDriverWait(driver, 10)
wait.until(EC.title_contains("Vaultwarden Web"))
driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_email').send_keys(
'${userEmail}'
)
driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_name').send_keys(
'A Cat'
)
driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_master-password').send_keys(
'${userPassword}'
)
driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_confirm-master-password').send_keys(
'${userPassword}'
)
if driver.find_element(By.CSS_SELECTOR, 'input#checkForBreaches').is_selected():
driver.find_element(By.CSS_SELECTOR, 'input#checkForBreaches').click()
driver.find_element(By.XPATH, "//button[contains(., 'Create account')]").click()
wait.until_not(EC.title_contains("Create account"))
driver.find_element(By.XPATH, "//button[contains(., 'Continue')]").click()
driver.find_element(By.CSS_SELECTOR, 'input#login_input_master-password').send_keys(
'${userPassword}'
)
driver.find_element(By.XPATH, "//button[contains(., 'Log in')]").click()
wait.until(EC.title_contains("Vaults"))
driver.find_element(By.XPATH, "//button[contains(., 'New item')]").click()
driver.find_element(By.CSS_SELECTOR, 'input#name').send_keys(
'secrets'
)
driver.find_element(By.CSS_SELECTOR, 'input#loginPassword').send_keys(
'${storedPassword}'
)
driver.find_element(By.XPATH, "//button[contains(., 'Save')]").click()
'';
in {
inherit name;
makeVaultwardenTest = backend: makeTest {
name = "vaultwarden-${backend}";
meta = {
maintainers = with pkgs.lib.maintainers; [ jjjollyjim ];
maintainers = with pkgs.lib.maintainers; [ dotlambda SuperSandro2000 ];
};
nodes = {
server = { pkgs, ... }:
let backendConfig = {
server = { pkgs, ... }: lib.mkMerge [
{
mysql = {
services.mysql = {
enable = true;
@ -53,113 +109,47 @@ let
postgresql = {
services.postgresql = {
enable = true;
initialScript = pkgs.writeText "postgresql-init.sql" ''
CREATE USER bitwardenuser WITH PASSWORD '${dbPassword}';
CREATE DATABASE bitwarden WITH OWNER bitwardenuser;
'';
ensureDatabases = [ "vaultwarden" ];
ensureUsers = [{
name = "vaultwarden";
ensureDBOwnership = true;
}];
};
services.vaultwarden.config.databaseUrl = "postgresql://bitwardenuser:${dbPassword}@localhost/bitwarden";
services.vaultwarden.config.databaseUrl = "postgresql:///vaultwarden?host=/run/postgresql";
systemd.services.vaultwarden.after = [ "postgresql.service" ];
};
sqlite = { };
};
in
mkMerge [
backendConfig.${backend}
{
services.vaultwarden = {
enable = true;
dbBackend = backend;
config = {
rocketAddress = "0.0.0.0";
rocketPort = 80;
};
};
sqlite = {
services.vaultwarden.backupDir = "/var/lib/vaultwarden/backups";
networking.firewall.allowedTCPPorts = [ 80 ];
environment.systemPackages = [ pkgs.sqlite ];
};
}.${backend}
environment.systemPackages =
let
testRunner = pkgs.writers.writePython3Bin "test-runner"
{
libraries = [ pkgs.python3Packages.selenium ];
flakeIgnore = [
"E501"
];
} ''
from selenium.webdriver.common.by import By
from selenium.webdriver import Firefox
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
options = Options()
options.add_argument('--headless')
driver = Firefox(options=options)
driver.implicitly_wait(20)
driver.get('http://localhost/#/register')
wait = WebDriverWait(driver, 10)
wait.until(EC.title_contains("Vaultwarden Web"))
driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_email').send_keys(
'${userEmail}'
)
driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_name').send_keys(
'A Cat'
)
driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_master-password').send_keys(
'${userPassword}'
)
driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_confirm-master-password').send_keys(
'${userPassword}'
)
if driver.find_element(By.CSS_SELECTOR, 'input#checkForBreaches').is_selected():
driver.find_element(By.CSS_SELECTOR, 'input#checkForBreaches').click()
driver.find_element(By.XPATH, "//button[contains(., 'Create account')]").click()
wait.until_not(EC.title_contains("Create account"))
driver.find_element(By.XPATH, "//button[contains(., 'Continue')]").click()
driver.find_element(By.CSS_SELECTOR, 'input#login_input_master-password').send_keys(
'${userPassword}'
)
driver.find_element(By.XPATH, "//button[contains(., 'Log in')]").click()
wait.until(EC.title_contains("Vaults"))
driver.find_element(By.XPATH, "//button[contains(., 'New item')]").click()
driver.find_element(By.CSS_SELECTOR, 'input#name').send_keys(
'secrets'
)
driver.find_element(By.CSS_SELECTOR, 'input#loginPassword').send_keys(
'${storedPassword}'
)
driver.find_element(By.XPATH, "//button[contains(., 'Save')]").click()
'';
in
[ pkgs.firefox-unwrapped pkgs.geckodriver testRunner ];
}
];
client = { pkgs, ... }:
{
environment.systemPackages = [ pkgs.bitwarden-cli ];
};
services.vaultwarden = {
enable = true;
dbBackend = backend;
config = {
rocketAddress = "0.0.0.0";
rocketPort = 80;
};
};
networking.firewall.allowedTCPPorts = [ 80 ];
environment.systemPackages = [ pkgs.firefox-unwrapped pkgs.geckodriver testRunner ];
}
];
} // lib.optionalAttrs withClient {
client = { pkgs, ... }: {
environment.systemPackages = [ pkgs.bitwarden-cli ];
};
};
testScript = ''
testScript = if testScript != null then testScript else ''
start_all()
server.wait_for_unit("vaultwarden.service")
server.wait_for_open_port(80)
@ -184,15 +174,37 @@ let
client.succeed(f"bw --nointeraction --raw --session {key} sync -f")
with subtest("get the password with the cli"):
password = client.succeed(
f"bw --nointeraction --raw --session {key} list items | ${pkgs.jq}/bin/jq -r .[].login.password"
password = client.wait_until_succeeds(
f"bw --nointeraction --raw --session {key} list items | ${pkgs.jq}/bin/jq -r .[].login.password",
timeout=60
)
assert password.strip() == "${storedPassword}"
'';
};
});
in
builtins.listToAttrs (
map
(backend: { name = backend; value = makeVaultwardenTest backend; })
backends
)
builtins.mapAttrs (k: v: makeVaultwardenTest k v) {
mysql = {};
postgresql = {};
sqlite = {};
sqlite-backup = {
backend = "sqlite";
withClient = false;
testScript = ''
start_all()
server.wait_for_unit("vaultwarden.service")
server.wait_for_open_port(80)
with subtest("Set up vaultwarden"):
server.succeed("PYTHONUNBUFFERED=1 test-runner | systemd-cat -t test-runner")
with subtest("Run the backup script"):
server.start_job("backup-vaultwarden.service")
with subtest("Check that backup exists"):
server.succeed('[ -d "/var/lib/vaultwarden/backups" ]')
server.succeed('[ -f "/var/lib/vaultwarden/backups/db.sqlite3" ]')
server.succeed('[ -d "/var/lib/vaultwarden/backups/attachments" ]')
'';
};
}