mirror of
https://github.com/dani-garcia/vaultwarden.wiki.git
synced 2026-10-26 04:06:48 +02:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76b2905aa2 | ||
|
|
73e05e2d3a | ||
|
|
b227a7c108 | ||
|
|
3ac3709d17 | ||
|
|
32098ca7d1 |
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
|
||||||
|
|
||||||
|
- 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
|
||||||
@@ -3,5 +3,3 @@ DROP TABLE IF EXISTS user_signature_key_pairs;
|
|||||||
ALTER TABLE users DROP COLUMN signed_public_key;
|
ALTER TABLE users DROP COLUMN signed_public_key;
|
||||||
ALTER TABLE users DROP COLUMN security_state;
|
ALTER TABLE users DROP COLUMN security_state;
|
||||||
ALTER TABLE users DROP COLUMN security_version;
|
ALTER TABLE users DROP COLUMN security_version;
|
||||||
ALTER TABLE users DROP COLUMN v2_upgrade_token;
|
|
||||||
ALTER TABLE users_organizations DROP COLUMN v2_upgrade_token;
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
ALTER TABLE users ADD COLUMN signed_public_key TEXT;
|
ALTER TABLE users ADD COLUMN signed_public_key TEXT;
|
||||||
ALTER TABLE users ADD COLUMN security_state TEXT;
|
ALTER TABLE users ADD COLUMN security_state TEXT;
|
||||||
ALTER TABLE users ADD COLUMN security_version INTEGER;
|
ALTER TABLE users ADD COLUMN security_version INTEGER;
|
||||||
ALTER TABLE users ADD COLUMN v2_upgrade_token TEXT;
|
|
||||||
ALTER TABLE users_organizations ADD COLUMN v2_upgrade_token TEXT;
|
|
||||||
|
|
||||||
CREATE TABLE user_signature_key_pairs (
|
CREATE TABLE user_signature_key_pairs (
|
||||||
uuid CHAR(36) NOT NULL PRIMARY KEY,
|
uuid CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users DROP COLUMN v2_upgrade_token;
|
||||||
|
ALTER TABLE users_organizations DROP COLUMN v2_upgrade_token;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN v2_upgrade_token TEXT;
|
||||||
|
ALTER TABLE users_organizations ADD COLUMN v2_upgrade_token TEXT;
|
||||||
@@ -3,5 +3,3 @@ DROP TABLE IF EXISTS user_signature_key_pairs;
|
|||||||
ALTER TABLE users DROP COLUMN signed_public_key;
|
ALTER TABLE users DROP COLUMN signed_public_key;
|
||||||
ALTER TABLE users DROP COLUMN security_state;
|
ALTER TABLE users DROP COLUMN security_state;
|
||||||
ALTER TABLE users DROP COLUMN security_version;
|
ALTER TABLE users DROP COLUMN security_version;
|
||||||
ALTER TABLE users DROP COLUMN v2_upgrade_token;
|
|
||||||
ALTER TABLE users_organizations DROP COLUMN v2_upgrade_token;
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
ALTER TABLE users ADD COLUMN signed_public_key TEXT;
|
ALTER TABLE users ADD COLUMN signed_public_key TEXT;
|
||||||
ALTER TABLE users ADD COLUMN security_state TEXT;
|
ALTER TABLE users ADD COLUMN security_state TEXT;
|
||||||
ALTER TABLE users ADD COLUMN security_version INTEGER;
|
ALTER TABLE users ADD COLUMN security_version INTEGER;
|
||||||
ALTER TABLE users ADD COLUMN v2_upgrade_token TEXT;
|
|
||||||
ALTER TABLE users_organizations ADD COLUMN v2_upgrade_token TEXT;
|
|
||||||
|
|
||||||
CREATE TABLE user_signature_key_pairs (
|
CREATE TABLE user_signature_key_pairs (
|
||||||
uuid CHAR(36) NOT NULL PRIMARY KEY,
|
uuid CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users DROP COLUMN v2_upgrade_token;
|
||||||
|
ALTER TABLE users_organizations DROP COLUMN v2_upgrade_token;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN v2_upgrade_token TEXT;
|
||||||
|
ALTER TABLE users_organizations ADD COLUMN v2_upgrade_token TEXT;
|
||||||
@@ -3,5 +3,3 @@ DROP TABLE IF EXISTS user_signature_key_pairs;
|
|||||||
ALTER TABLE users DROP COLUMN signed_public_key;
|
ALTER TABLE users DROP COLUMN signed_public_key;
|
||||||
ALTER TABLE users DROP COLUMN security_state;
|
ALTER TABLE users DROP COLUMN security_state;
|
||||||
ALTER TABLE users DROP COLUMN security_version;
|
ALTER TABLE users DROP COLUMN security_version;
|
||||||
ALTER TABLE users DROP COLUMN v2_upgrade_token;
|
|
||||||
ALTER TABLE users_organizations DROP COLUMN v2_upgrade_token;
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
ALTER TABLE users ADD COLUMN signed_public_key TEXT;
|
ALTER TABLE users ADD COLUMN signed_public_key TEXT;
|
||||||
ALTER TABLE users ADD COLUMN security_state TEXT;
|
ALTER TABLE users ADD COLUMN security_state TEXT;
|
||||||
ALTER TABLE users ADD COLUMN security_version INTEGER;
|
ALTER TABLE users ADD COLUMN security_version INTEGER;
|
||||||
ALTER TABLE users ADD COLUMN v2_upgrade_token TEXT;
|
|
||||||
ALTER TABLE users_organizations ADD COLUMN v2_upgrade_token TEXT;
|
|
||||||
|
|
||||||
CREATE TABLE user_signature_key_pairs (
|
CREATE TABLE user_signature_key_pairs (
|
||||||
uuid TEXT NOT NULL PRIMARY KEY,
|
uuid TEXT NOT NULL PRIMARY KEY,
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users DROP COLUMN v2_upgrade_token;
|
||||||
|
ALTER TABLE users_organizations DROP COLUMN v2_upgrade_token;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN v2_upgrade_token TEXT;
|
||||||
|
ALTER TABLE users_organizations ADD COLUMN v2_upgrade_token TEXT;
|
||||||
@@ -2268,8 +2268,15 @@ impl CipherSyncData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate a HashMap with the Organization UUID as key and the Membership record
|
||||||
|
let members: HashMap<OrganizationId, Membership> = Membership::find_confirmed_by_user(user_id, conn)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| (m.org_uuid.clone(), m))
|
||||||
|
.collect();
|
||||||
|
|
||||||
// Generate a list of Cipher UUID's containing a Vec with one or more Attachment records
|
// Generate a list of Cipher UUID's containing a Vec with one or more Attachment records
|
||||||
let orgs = Membership::get_orgs_by_user(user_id, conn).await;
|
let orgs: Vec<OrganizationId> = members.keys().cloned().collect();
|
||||||
let attachments = Attachment::find_all_by_user_and_orgs(user_id, &orgs, conn).await;
|
let attachments = Attachment::find_all_by_user_and_orgs(user_id, &orgs, conn).await;
|
||||||
let mut cipher_attachments: HashMap<CipherId, Vec<Attachment>> = HashMap::with_capacity(attachments.len());
|
let mut cipher_attachments: HashMap<CipherId, Vec<Attachment>> = HashMap::with_capacity(attachments.len());
|
||||||
for attachment in attachments {
|
for attachment in attachments {
|
||||||
@@ -2284,13 +2291,6 @@ impl CipherSyncData {
|
|||||||
cipher_collections.entry(cipher).or_default().push(collection);
|
cipher_collections.entry(cipher).or_default().push(collection);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a HashMap with the Organization UUID as key and the Membership record
|
|
||||||
let members: HashMap<OrganizationId, Membership> = Membership::find_confirmed_by_user(user_id, conn)
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.map(|m| (m.org_uuid.clone(), m))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Generate a HashMap with the User_Collections UUID as key and the CollectionUser record
|
// Generate a HashMap with the User_Collections UUID as key and the CollectionUser record
|
||||||
let user_collections: HashMap<CollectionId, CollectionUser> = CollectionUser::find_by_user(user_id, conn)
|
let user_collections: HashMap<CollectionId, CollectionUser> = CollectionUser::find_by_user(user_id, conn)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -415,8 +415,9 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea
|
|||||||
let col_users = CollectionUser::find_by_organization_swap_user_uuid_with_member_uuid(&org_id, &conn).await;
|
let col_users = CollectionUser::find_by_organization_swap_user_uuid_with_member_uuid(&org_id, &conn).await;
|
||||||
// Generate a HashMap to get the correct MembershipType per user to determine the manage permission
|
// Generate a HashMap to get the correct MembershipType per user to determine the manage permission
|
||||||
// We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser
|
// We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser
|
||||||
|
// This lists other members for admins, so it must not depend on the membership status
|
||||||
let membership_type: HashMap<MembershipId, i32> =
|
let membership_type: HashMap<MembershipId, i32> =
|
||||||
Membership::find_confirmed_by_org(&org_id, &conn).await.into_iter().map(|m| (m.uuid, m.atype)).collect();
|
Membership::find_by_org(&org_id, &conn).await.into_iter().map(|m| (m.uuid, m.atype)).collect();
|
||||||
|
|
||||||
// check if current user has full access to the organization (either directly or via any group)
|
// check if current user has full access to the organization (either directly or via any group)
|
||||||
let has_full_access_to_org = member.has_full_access()
|
let has_full_access_to_org = member.has_full_access()
|
||||||
@@ -818,11 +819,9 @@ async fn get_org_collection_detail(
|
|||||||
|
|
||||||
// Generate a HashMap to get the correct MembershipType per user to determine the manage permission
|
// Generate a HashMap to get the correct MembershipType per user to determine the manage permission
|
||||||
// We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser
|
// We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser
|
||||||
let membership_type: HashMap<MembershipId, i32> = Membership::find_confirmed_by_org(&org_id, &conn)
|
// This lists other members for admins, so it must not depend on the membership status
|
||||||
.await
|
let membership_type: HashMap<MembershipId, i32> =
|
||||||
.into_iter()
|
Membership::find_by_org(&org_id, &conn).await.into_iter().map(|m| (m.uuid, m.atype)).collect();
|
||||||
.map(|m| (m.uuid, m.atype))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let users: Vec<Value> =
|
let users: Vec<Value> =
|
||||||
CollectionUser::find_by_org_and_coll_swap_user_uuid_with_member_uuid(&org_id, &collection.uuid, &conn)
|
CollectionUser::find_by_org_and_coll_swap_user_uuid_with_member_uuid(&org_id, &collection.uuid, &conn)
|
||||||
@@ -960,8 +959,9 @@ async fn get_members(
|
|||||||
|
|
||||||
let mut users_json = Vec::new();
|
let mut users_json = Vec::new();
|
||||||
for u in Membership::find_by_org(&org_id, &conn).await {
|
for u in Membership::find_by_org(&org_id, &conn).await {
|
||||||
|
// The user can be a manager instead of an admin, but we've checked above that they have full access
|
||||||
users_json.push(
|
users_json.push(
|
||||||
u.to_json_user_details(
|
u.to_json_details_for_admin(
|
||||||
data.include_collections.unwrap_or(false),
|
data.include_collections.unwrap_or(false),
|
||||||
data.include_groups.unwrap_or(false),
|
data.include_groups.unwrap_or(false),
|
||||||
&conn,
|
&conn,
|
||||||
@@ -1516,7 +1516,9 @@ async fn get_user(
|
|||||||
// In this case, when groups are requested we also need to include collections.
|
// In this case, when groups are requested we also need to include collections.
|
||||||
// Else these will not be shown in the interface, and could lead to missing collections when saved.
|
// Else these will not be shown in the interface, and could lead to missing collections when saved.
|
||||||
let include_groups = data.include_groups.unwrap_or(false);
|
let include_groups = data.include_groups.unwrap_or(false);
|
||||||
Ok(Json(user.to_json_user_details(data.include_collections.unwrap_or(include_groups), include_groups, &conn).await))
|
Ok(Json(
|
||||||
|
user.to_json_details_for_admin(data.include_collections.unwrap_or(include_groups), include_groups, &conn).await,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -1835,6 +1837,10 @@ async fn post_org_import(
|
|||||||
if org_id != headers.membership.org_uuid {
|
if org_id != headers.membership.org_uuid {
|
||||||
err!("Organization not found", "Organization id's do not match");
|
err!("Organization not found", "Organization id's do not match");
|
||||||
}
|
}
|
||||||
|
// OrgMemberHeaders also allows invited and accepted members, which are not allowed to import
|
||||||
|
if headers.membership.status != MembershipStatus::Confirmed as i32 {
|
||||||
|
err!("You need to be a Member of the Organization to call this endpoint")
|
||||||
|
}
|
||||||
let data: ImportData = data.into_inner();
|
let data: ImportData = data.into_inner();
|
||||||
|
|
||||||
// Validate the import before continuing
|
// Validate the import before continuing
|
||||||
|
|||||||
@@ -1040,6 +1040,7 @@ impl Cipher {
|
|||||||
.eq(ciphers_collections::collection_uuid)
|
.eq(ciphers_collections::collection_uuid)
|
||||||
.and(collections_groups::groups_uuid.eq(groups::uuid))),
|
.and(collections_groups::groups_uuid.eq(groups::uuid))),
|
||||||
)
|
)
|
||||||
|
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
|
||||||
.filter(
|
.filter(
|
||||||
users_organizations::access_all
|
users_organizations::access_all
|
||||||
.eq(true) // User has access all
|
.eq(true) // User has access all
|
||||||
@@ -1071,6 +1072,7 @@ impl Cipher {
|
|||||||
.eq(ciphers_collections::collection_uuid)
|
.eq(ciphers_collections::collection_uuid)
|
||||||
.and(users_collections::user_uuid.eq(user_uuid.clone()))),
|
.and(users_collections::user_uuid.eq(user_uuid.clone()))),
|
||||||
)
|
)
|
||||||
|
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
|
||||||
.filter(
|
.filter(
|
||||||
users_organizations::access_all
|
users_organizations::access_all
|
||||||
.eq(true) // User has access all
|
.eq(true) // User has access all
|
||||||
@@ -1115,6 +1117,7 @@ impl Cipher {
|
|||||||
.eq(ciphers_collections::collection_uuid)
|
.eq(ciphers_collections::collection_uuid)
|
||||||
.and(collections_groups::groups_uuid.eq(groups::uuid))),
|
.and(collections_groups::groups_uuid.eq(groups::uuid))),
|
||||||
)
|
)
|
||||||
|
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
|
||||||
.filter(
|
.filter(
|
||||||
users_organizations::access_all
|
users_organizations::access_all
|
||||||
.eq(true) // User has access all
|
.eq(true) // User has access all
|
||||||
@@ -1147,6 +1150,7 @@ impl Cipher {
|
|||||||
.eq(ciphers_collections::collection_uuid)
|
.eq(ciphers_collections::collection_uuid)
|
||||||
.and(users_collections::user_uuid.eq(user_uuid.clone()))),
|
.and(users_collections::user_uuid.eq(user_uuid.clone()))),
|
||||||
)
|
)
|
||||||
|
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
|
||||||
.filter(
|
.filter(
|
||||||
users_organizations::access_all
|
users_organizations::access_all
|
||||||
.eq(true) // User has access all
|
.eq(true) // User has access all
|
||||||
@@ -1198,6 +1202,7 @@ impl Cipher {
|
|||||||
.or_filter(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner
|
.or_filter(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner
|
||||||
.or_filter(groups::access_all.eq(true)) //Access via group
|
.or_filter(groups::access_all.eq(true)) //Access via group
|
||||||
.or_filter(collections_groups::collections_uuid.is_not_null()) //Access via group
|
.or_filter(collections_groups::collections_uuid.is_not_null()) //Access via group
|
||||||
|
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
|
||||||
.select(ciphers_collections::all_columns)
|
.select(ciphers_collections::all_columns)
|
||||||
.distinct()
|
.distinct()
|
||||||
.load::<(CipherId, CollectionId)>(conn)
|
.load::<(CipherId, CollectionId)>(conn)
|
||||||
|
|||||||
@@ -367,6 +367,7 @@ impl Collection {
|
|||||||
.and(collections_groups::collections_uuid.eq(collections::uuid))),
|
.and(collections_groups::collections_uuid.eq(collections::uuid))),
|
||||||
)
|
)
|
||||||
.filter(collections::uuid.eq(uuid))
|
.filter(collections::uuid.eq(uuid))
|
||||||
|
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
|
||||||
.filter(
|
.filter(
|
||||||
users_collections::collection_uuid
|
users_collections::collection_uuid
|
||||||
.eq(uuid)
|
.eq(uuid)
|
||||||
@@ -406,6 +407,7 @@ impl Collection {
|
|||||||
.and(users_organizations::user_uuid.eq(user_uuid))),
|
.and(users_organizations::user_uuid.eq(user_uuid))),
|
||||||
)
|
)
|
||||||
.filter(collections::uuid.eq(uuid))
|
.filter(collections::uuid.eq(uuid))
|
||||||
|
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
|
||||||
.filter(users_collections::collection_uuid.eq(uuid).or(
|
.filter(users_collections::collection_uuid.eq(uuid).or(
|
||||||
// Directly accessed collection
|
// Directly accessed collection
|
||||||
users_organizations::access_all.eq(true).or(
|
users_organizations::access_all.eq(true).or(
|
||||||
@@ -430,7 +432,8 @@ impl Collection {
|
|||||||
.inner_join(
|
.inner_join(
|
||||||
users_organizations::table.on(collections::org_uuid
|
users_organizations::table.on(collections::org_uuid
|
||||||
.eq(users_organizations::org_uuid)
|
.eq(users_organizations::org_uuid)
|
||||||
.and(users_organizations::user_uuid.eq(user_uuid.clone()))),
|
.and(users_organizations::user_uuid.eq(user_uuid.clone()))
|
||||||
|
.and(users_organizations::status.eq(MembershipStatus::Confirmed as i32))),
|
||||||
)
|
)
|
||||||
.left_join(
|
.left_join(
|
||||||
users_collections::table.on(users_collections::collection_uuid
|
users_collections::table.on(users_collections::collection_uuid
|
||||||
@@ -476,7 +479,8 @@ impl Collection {
|
|||||||
.inner_join(
|
.inner_join(
|
||||||
users_organizations::table.on(collections::org_uuid
|
users_organizations::table.on(collections::org_uuid
|
||||||
.eq(users_organizations::org_uuid)
|
.eq(users_organizations::org_uuid)
|
||||||
.and(users_organizations::user_uuid.eq(user_uuid.clone()))),
|
.and(users_organizations::user_uuid.eq(user_uuid.clone()))
|
||||||
|
.and(users_organizations::status.eq(MembershipStatus::Confirmed as i32))),
|
||||||
)
|
)
|
||||||
.left_join(
|
.left_join(
|
||||||
users_collections::table.on(users_collections::collection_uuid
|
users_collections::table.on(users_collections::collection_uuid
|
||||||
@@ -527,6 +531,7 @@ impl Collection {
|
|||||||
.and(collections_groups::collections_uuid.eq(collections::uuid))),
|
.and(collections_groups::collections_uuid.eq(collections::uuid))),
|
||||||
)
|
)
|
||||||
.filter(collections::uuid.eq(&self.uuid))
|
.filter(collections::uuid.eq(&self.uuid))
|
||||||
|
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
|
||||||
.filter(
|
.filter(
|
||||||
users_collections::collection_uuid
|
users_collections::collection_uuid
|
||||||
.eq(&self.uuid)
|
.eq(&self.uuid)
|
||||||
@@ -586,6 +591,7 @@ impl Collection {
|
|||||||
.and(collections_groups::collections_uuid.eq(collections::uuid))),
|
.and(collections_groups::collections_uuid.eq(collections::uuid))),
|
||||||
)
|
)
|
||||||
.filter(collections::uuid.eq(&uuid))
|
.filter(collections::uuid.eq(&uuid))
|
||||||
|
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
|
||||||
.filter(
|
.filter(
|
||||||
users_collections::collection_uuid
|
users_collections::collection_uuid
|
||||||
.eq(&uuid)
|
.eq(&uuid)
|
||||||
@@ -650,6 +656,7 @@ impl Collection {
|
|||||||
.and(collections_groups::collections_uuid.eq(collections::uuid))),
|
.and(collections_groups::collections_uuid.eq(collections::uuid))),
|
||||||
)
|
)
|
||||||
.filter(collections::org_uuid.eq(&org_uuid))
|
.filter(collections::org_uuid.eq(&org_uuid))
|
||||||
|
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
|
||||||
.filter(
|
.filter(
|
||||||
// Manage permission on a collection assigned directly or via a group.
|
// Manage permission on a collection assigned directly or via a group.
|
||||||
users_collections::manage.eq(true).or(collections_groups::manage.eq(true)),
|
users_collections::manage.eq(true).or(collections_groups::manage.eq(true)),
|
||||||
@@ -819,10 +826,18 @@ impl CollectionUser {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only returns the collections of organizations the user is a confirmed member of
|
||||||
pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
|
pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
|
||||||
conn.run(move |conn| {
|
conn.run(move |conn| {
|
||||||
users_collections::table
|
users_collections::table
|
||||||
|
.inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid)))
|
||||||
|
.inner_join(
|
||||||
|
users_organizations::table.on(users_organizations::org_uuid
|
||||||
|
.eq(collections::org_uuid)
|
||||||
|
.and(users_organizations::user_uuid.eq(users_collections::user_uuid))),
|
||||||
|
)
|
||||||
.filter(users_collections::user_uuid.eq(user_uuid))
|
.filter(users_collections::user_uuid.eq(user_uuid))
|
||||||
|
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
|
||||||
.select(users_collections::all_columns)
|
.select(users_collections::all_columns)
|
||||||
.load::<Self>(conn)
|
.load::<Self>(conn)
|
||||||
.expect("Error loading users_collections")
|
.expect("Error loading users_collections")
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ impl Group {
|
|||||||
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))),
|
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))),
|
||||||
)
|
)
|
||||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||||
|
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
|
||||||
.filter(groups::access_all.eq(true))
|
.filter(groups::access_all.eq(true))
|
||||||
.select(groups::organizations_uuid)
|
.select(groups::organizations_uuid)
|
||||||
.distinct()
|
.distinct()
|
||||||
@@ -387,6 +388,7 @@ impl CollectionGroup {
|
|||||||
.and(collections::org_uuid.eq(groups::organizations_uuid))),
|
.and(collections::org_uuid.eq(groups::organizations_uuid))),
|
||||||
)
|
)
|
||||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||||
|
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
|
||||||
.select(collections_groups::all_columns)
|
.select(collections_groups::all_columns)
|
||||||
.load::<Self>(conn)
|
.load::<Self>(conn)
|
||||||
.expect("Error loading user collection groups")
|
.expect("Error loading user collection groups")
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
use std::{
|
use std::cmp::Ordering;
|
||||||
cmp::Ordering,
|
|
||||||
collections::{HashMap, HashSet},
|
|
||||||
};
|
|
||||||
|
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use derive_more::{AsRef, Deref, Display, From};
|
use derive_more::{AsRef, Deref, Display, From};
|
||||||
@@ -24,8 +21,8 @@ use crate::{
|
|||||||
use macros::UuidFromParam;
|
use macros::UuidFromParam;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
Cipher, CipherId, Collection, CollectionGroup, CollectionId, CollectionUser, Group, GroupId, GroupUser, OrgPolicy,
|
Cipher, CipherId, Collection, CollectionId, CollectionUser, Group, GroupId, GroupUser, OrgPolicy, OrgPolicyType,
|
||||||
OrgPolicyType, TwoFactor, User, UserId,
|
TwoFactor, User, UserId,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||||
@@ -541,7 +538,13 @@ impl Membership {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn to_json_user_details(&self, include_collections: bool, include_groups: bool, conn: &DbConn) -> Value {
|
// Used by admins to view other members, so nothing here may depend on this member's status
|
||||||
|
pub async fn to_json_details_for_admin(
|
||||||
|
&self,
|
||||||
|
include_collections: bool,
|
||||||
|
include_groups: bool,
|
||||||
|
conn: &DbConn,
|
||||||
|
) -> Value {
|
||||||
let user = User::find_by_uuid(&self.user_uuid, conn).await.unwrap();
|
let user = User::find_by_uuid(&self.user_uuid, conn).await.unwrap();
|
||||||
|
|
||||||
// Because BitWarden want the status to be -1 for revoked users we need to catch that here.
|
// Because BitWarden want the status to be -1 for revoked users we need to catch that here.
|
||||||
@@ -564,52 +567,23 @@ impl Membership {
|
|||||||
|
|
||||||
// Check if a user is in a group which has access to all collections
|
// Check if a user is in a group which has access to all collections
|
||||||
// If that is the case, we should not return individual collections!
|
// If that is the case, we should not return individual collections!
|
||||||
|
// This is used by admins to view other members, so it must not depend on the membership status
|
||||||
let full_access_group =
|
let full_access_group =
|
||||||
CONFIG.org_groups_enabled() && Group::is_in_full_access_group(&self.user_uuid, &self.org_uuid, conn).await;
|
CONFIG.org_groups_enabled() && GroupUser::has_full_access_by_member(&self.org_uuid, &self.uuid, conn).await;
|
||||||
|
|
||||||
// If collections are to be included, only include them if the user does not have full access via a group or defined to the user it self
|
// If collections are to be included, only include them if the user does not have full access via a group or defined to the user it self
|
||||||
|
// Only the collections assigned directly are returned, the ones assigned via a group are returned via a special group endpoint
|
||||||
let collections: Vec<Value> = if include_collections && !(full_access_group || self.access_all) {
|
let collections: Vec<Value> = if include_collections && !(full_access_group || self.access_all) {
|
||||||
// Get all collections for the user here already to prevent more queries
|
CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
|
||||||
let cu: HashMap<CollectionId, CollectionUser> =
|
|
||||||
CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.map(|cu| (cu.collection_uuid.clone(), cu))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Get all collection groups for this user to prevent there inclusion
|
|
||||||
let cg: HashSet<CollectionId> = CollectionGroup::find_by_user(&self.user_uuid, conn)
|
|
||||||
.await
|
.await
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|cg| cg.collections_uuid)
|
.map(|cu| {
|
||||||
.collect();
|
json!({
|
||||||
|
"id": cu.collection_uuid,
|
||||||
Collection::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
|
"readOnly": cu.read_only,
|
||||||
.await
|
"hidePasswords": cu.hide_passwords,
|
||||||
.into_iter()
|
"manage": cu.manage || (self.atype == MembershipType::Manager && !cu.read_only && !cu.hide_passwords),
|
||||||
.filter_map(|c| {
|
})
|
||||||
let (read_only, hide_passwords, manage) = if self.has_full_access() {
|
|
||||||
(false, false, self.atype >= MembershipType::Manager)
|
|
||||||
} else if let Some(cu) = cu.get(&c.uuid) {
|
|
||||||
(
|
|
||||||
cu.read_only,
|
|
||||||
cu.hide_passwords,
|
|
||||||
cu.manage || (self.atype == MembershipType::Manager && !cu.read_only && !cu.hide_passwords),
|
|
||||||
)
|
|
||||||
// If previous checks failed it might be that this user has access via a group, but we should not return those elements here
|
|
||||||
// Those are returned via a special group endpoint
|
|
||||||
} else if cg.contains(&c.uuid) {
|
|
||||||
return None;
|
|
||||||
} else {
|
|
||||||
(true, true, false)
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(json!({
|
|
||||||
"id": c.uuid,
|
|
||||||
"readOnly": read_only,
|
|
||||||
"hidePasswords": hide_passwords,
|
|
||||||
"manage": manage,
|
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
@@ -1037,17 +1011,6 @@ impl Membership {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_orgs_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<OrganizationId> {
|
|
||||||
conn.run(move |conn| {
|
|
||||||
users_organizations::table
|
|
||||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
|
||||||
.select(users_organizations::org_uuid)
|
|
||||||
.load::<OrganizationId>(conn)
|
|
||||||
.unwrap_or_default()
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn find_by_user_and_policy(user_uuid: &UserId, policy_type: OrgPolicyType, conn: &DbConn) -> Vec<Self> {
|
pub async fn find_by_user_and_policy(user_uuid: &UserId, policy_type: OrgPolicyType, conn: &DbConn) -> Vec<Self> {
|
||||||
conn.run(move |conn| {
|
conn.run(move |conn| {
|
||||||
users_organizations::table
|
users_organizations::table
|
||||||
@@ -1131,6 +1094,7 @@ impl Membership {
|
|||||||
.and(ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()))),
|
.and(ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()))),
|
||||||
)
|
)
|
||||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||||
|
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
|
||||||
.filter(
|
.filter(
|
||||||
users_organizations::atype.eq_any(vec![MembershipType::Owner as i32, MembershipType::Admin as i32]),
|
users_organizations::atype.eq_any(vec![MembershipType::Owner as i32, MembershipType::Admin as i32]),
|
||||||
)
|
)
|
||||||
|
|||||||
Executable
+120
@@ -0,0 +1,120 @@
|
|||||||
|
#!/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"],
|
||||||
|
"name": vector["name"],
|
||||||
|
"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 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)
|
||||||
|
request = urllib.request.Request(
|
||||||
|
f"{server}/identity/accounts/register",
|
||||||
|
data=json.dumps(body).encode(),
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request):
|
||||||
|
pass
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
sys.exit(f"Registering {body['email']} failed: {e.code} {e.read().decode()}")
|
||||||
|
|
||||||
|
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`).
|
||||||
|
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