mirror of
https://github.com/dani-garcia/vaultwarden.wiki.git
synced 2026-10-23 01:58:09 +03:00
Add a manual workflow that runs the SDK live integration tests
This commit is contained in:
3 files changed
+255
No files matched your search
@@ -0,0 +1,65 @@
|
||||
name: SDK live tests
|
||||
permissions: {}
|
||||
|
||||
# Runs the live-server integration tests of bitwarden/sdk-internal against this branch.
|
||||
# Only started by hand: it builds the SDK too, and the tests aren't in a released SDK yet.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sdk_ref:
|
||||
description: "Branch, tag or commit of bitwarden/sdk-internal to test with"
|
||||
required: true
|
||||
default: "km/live-server-integration-tests"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
sdk-live-tests:
|
||||
name: SDK live tests
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- name: "Install dependencies Ubuntu"
|
||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends build-essential libssl-dev pkg-config
|
||||
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Checkout sdk-internal"
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: bitwarden/sdk-internal
|
||||
ref: ${{ inputs.sdk_ref }}
|
||||
path: sdk-internal
|
||||
persist-credentials: false
|
||||
|
||||
# Each checkout pins its own toolchain in rust-toolchain.toml
|
||||
- name: "Install toolchains"
|
||||
run: |
|
||||
rustup toolchain install
|
||||
cd sdk-internal
|
||||
rustup toolchain install
|
||||
rustup target add wasm32-unknown-unknown
|
||||
rustup component add rust-src
|
||||
|
||||
- name: "Setup Node"
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: "Install binaryen"
|
||||
run: npm i -g binaryen@132.0.0
|
||||
|
||||
- name: "Run the SDK live tests"
|
||||
run: tools/sdk-live-tests/run.sh --sdk-dir sdk-internal
|
||||
|
||||
- name: "Upload logs"
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: sdk-live-tests-logs
|
||||
path: target/sdk-live-tests/**/*.log
|
||||
Executable
+129
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Registers the account of an sdk-internal test vector on a vaultwarden server, as a client would.
|
||||
|
||||
Usage: register_vector.py <vector.json> <server-url>
|
||||
|
||||
A V1 vector is registered with the flat `keys` object, a V2 one with `accountKeys` and the user key
|
||||
id. Prints the account's email and password on two lines, for the caller to log in with.
|
||||
|
||||
Exits with SKIP, and the reason on stderr, for a vector the live tests can't use.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
SKIP = 3
|
||||
MIN_PBKDF2_ITERATIONS = 100_000
|
||||
|
||||
|
||||
def skip_reason(vector):
|
||||
if not any("masterPasswordUnlock" in m for m in vector["unlockMethods"]):
|
||||
return "it has no master password, which the live tests log in with"
|
||||
pbkdf2 = vector["account"]["kdf"].get("pBKDF2")
|
||||
if pbkdf2 is not None and pbkdf2["iterations"] < MIN_PBKDF2_ITERATIONS:
|
||||
return f"registration requires at least {MIN_PBKDF2_ITERATIONS} PBKDF2 iterations (upstream 600000)"
|
||||
return None
|
||||
|
||||
|
||||
def kdf_of(account):
|
||||
kind, params = next(iter(account["kdf"].items()))
|
||||
if kind == "pBKDF2":
|
||||
return {"kdfType": 0, "iterations": params["iterations"]}
|
||||
return {
|
||||
"kdfType": 1,
|
||||
"iterations": params["iterations"],
|
||||
"memory": params["memory"],
|
||||
"parallelism": params["parallelism"],
|
||||
}
|
||||
|
||||
|
||||
def register_body(vector):
|
||||
account = vector["account"]
|
||||
raw = vector["rawCryptographicState"]
|
||||
version, state = next(iter(account["accountCryptographicState"].items()))
|
||||
kdf = kdf_of(account)
|
||||
unlock = next(m["masterPasswordUnlock"] for m in vector["unlockMethods"] if "masterPasswordUnlock" in m)
|
||||
mp_unlock = unlock["master_password_unlock"]
|
||||
|
||||
body = {
|
||||
"email": account["email"],
|
||||
"masterPasswordHint": None,
|
||||
"masterPasswordAuthentication": {
|
||||
"kdf": kdf,
|
||||
"salt": mp_unlock["salt"],
|
||||
"masterPasswordAuthenticationHash": vector["masterPasswordAuthenticationHash"],
|
||||
},
|
||||
"masterPasswordUnlock": {
|
||||
"kdf": kdf,
|
||||
"salt": mp_unlock["salt"],
|
||||
"masterKeyWrappedUserKey": mp_unlock["masterKeyWrappedUserKey"],
|
||||
},
|
||||
}
|
||||
|
||||
if version == "V2":
|
||||
body["masterPasswordUnlock"]["containedKeyId"] = raw["userKeyId"]
|
||||
body["accountKeys"] = {
|
||||
"userKeyEncryptedAccountPrivateKey": state["private_key"],
|
||||
"accountPublicKey": raw["publicKey"],
|
||||
"publicKeyEncryptionKeyPair": {
|
||||
"wrappedPrivateKey": state["private_key"],
|
||||
"publicKey": raw["publicKey"],
|
||||
"signedPublicKey": state["signed_public_key"],
|
||||
},
|
||||
"signatureKeyPair": {
|
||||
"signatureAlgorithm": "ed25519",
|
||||
"wrappedSigningKey": state["signing_key"],
|
||||
"verifyingKey": raw["verifyingKey"],
|
||||
},
|
||||
"securityState": {
|
||||
"securityState": state["security_state"],
|
||||
"securityVersion": account["securityVersion"],
|
||||
},
|
||||
}
|
||||
else:
|
||||
body["keys"] = {"encryptedPrivateKey": state["private_key"], "publicKey": raw["publicKey"]}
|
||||
|
||||
return body, unlock["password"]
|
||||
|
||||
|
||||
def post(url, body):
|
||||
request = urllib.request.Request(
|
||||
url,
|
||||
data=json.dumps(body).encode(),
|
||||
headers={"Content-Type": "application/json", "Accept": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request) as response:
|
||||
return response.read()
|
||||
except urllib.error.HTTPError as e:
|
||||
sys.exit(f"Registering {body['email']} failed at {url}: {e.code} {e.read().decode()}")
|
||||
|
||||
|
||||
def main():
|
||||
with open(sys.argv[1]) as f:
|
||||
vector = json.load(f)
|
||||
server = sys.argv[2].rstrip("/")
|
||||
|
||||
reason = skip_reason(vector)
|
||||
if reason is not None:
|
||||
print(f"Skipped: {reason}", file=sys.stderr)
|
||||
sys.exit(SKIP)
|
||||
|
||||
body, password = register_body(vector)
|
||||
# With signup verification off, the server returns the token instead of mailing it
|
||||
token = post(
|
||||
f"{server}/identity/accounts/register/send-verification-email",
|
||||
{"email": body["email"], "name": vector["name"]},
|
||||
)
|
||||
body["emailVerificationToken"] = json.loads(token)
|
||||
post(f"{server}/identity/accounts/register/finish", body)
|
||||
|
||||
print(body["email"])
|
||||
print(password)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs the live-server integration tests of bitwarden/sdk-internal against vaultwarden.
|
||||
#
|
||||
# Usage: tools/sdk-live-tests/run.sh --sdk-dir <path> [--skip-sdk-build]
|
||||
#
|
||||
# Every test vector of the SDK (test-vectors/users/) that the tests can log in with gets a fresh
|
||||
# server and database, since the tests rotate the account's keys. Stops at the first failure; the
|
||||
# server logs are in target/sdk-live-tests/. Building the SDK needs its Rust toolchain with the
|
||||
# wasm32-unknown-unknown target and rust-src, Node.js, and binaryen (`npm i -g binaryen@132.0.0`).
|
||||
set -euo pipefail
|
||||
|
||||
VW_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
OUT_DIR="${VW_DIR}/target/sdk-live-tests"
|
||||
URL="http://127.0.0.1:8099"
|
||||
SKIP=3 # register_vector.py's exit code for a vector the tests can't use
|
||||
|
||||
SDK_DIR=""
|
||||
BUILD_SDK=1
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--sdk-dir) SDK_DIR="$(cd "$2" && pwd)"; shift 2 ;;
|
||||
--skip-sdk-build) BUILD_SDK=0; shift ;;
|
||||
*) echo "Unknown argument: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[[ -n "${SDK_DIR}" ]] || { echo "--sdk-dir is required" >&2; exit 2; }
|
||||
TESTS_DIR="${SDK_DIR}/crates/bitwarden-wasm-internal/integration-tests"
|
||||
|
||||
(cd "${VW_DIR}" && cargo build --features sqlite)
|
||||
if [[ ${BUILD_SDK} -eq 1 ]]; then
|
||||
bash "${SDK_DIR}/crates/bitwarden-wasm-internal/build.sh"
|
||||
(cd "${TESTS_DIR}" && npm ci)
|
||||
fi
|
||||
|
||||
trap 'kill $(jobs -p) 2>/dev/null || true' EXIT
|
||||
|
||||
for vector_file in "${SDK_DIR}"/test-vectors/users/*.json; do
|
||||
vector="$(basename "${vector_file}" .json)"
|
||||
echo "=== ${vector}"
|
||||
data_dir="${OUT_DIR}/${vector}"
|
||||
rm -rf "${data_dir}" && mkdir -p "${data_dir}"
|
||||
|
||||
DATA_FOLDER="${data_dir}" DATABASE_URL="sqlite://${data_dir}/db.sqlite3" \
|
||||
ROCKET_ADDRESS=127.0.0.1 ROCKET_PORT=8099 DOMAIN="${URL}" WEB_VAULT_ENABLED=false \
|
||||
SIGNUPS_ALLOWED=true SIGNUPS_VERIFY=false LOGIN_RATELIMIT_MAX_BURST=1000 \
|
||||
"${VW_DIR}/target/debug/vaultwarden" > "${data_dir}/vaultwarden.log" 2>&1 &
|
||||
server=$!
|
||||
curl -sf --retry 30 --retry-connrefused --retry-delay 1 "${URL}/alive" > /dev/null
|
||||
|
||||
status=0
|
||||
credentials="$(python3 "${VW_DIR}/tools/sdk-live-tests/register_vector.py" "${vector_file}" "${URL}")" || status=$?
|
||||
if [[ ${status} -eq 0 ]]; then
|
||||
(cd "${TESTS_DIR}" && BW_LIVE_SERVER_URL="${URL}" BW_LIVE_EMAIL="$(sed -n 1p <<< "${credentials}")" \
|
||||
BW_LIVE_PASSWORD="$(sed -n 2p <<< "${credentials}")" npm run test:live)
|
||||
elif [[ ${status} -ne ${SKIP} ]]; then
|
||||
exit "${status}"
|
||||
fi
|
||||
|
||||
kill "${server}" && wait "${server}" || true
|
||||
done
|
||||
echo "All test vectors passed"
|
||||
Reference in new issue
Block a user