1
0
Fork 0
mirror of https://github.com/dani-garcia/vaultwarden.git synced 2025-06-09 12:33:53 +00:00

WIP Sync with Upstream

WIP on syncing API Responses with upstream.
This to prevent issues with new clients, and find possible current issues like members, collections, groups etc..

Signed-off-by: BlackDex <black.dex@gmail.com>
This commit is contained in:
BlackDex 2025-04-23 22:09:23 +02:00
parent 3a44dc963b
commit bdb46fee2d
No known key found for this signature in database
GPG key ID: 58C80A2AA6C765E1
11 changed files with 104 additions and 40 deletions

31
Cargo.lock generated
View file

@ -409,9 +409,9 @@ dependencies = [
[[package]]
name = "brotli"
version = "8.0.1"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d"
checksum = "cf19e729cdbd51af9a397fb9ef8ac8378007b797f8273cfbfdf45dcaa316167b"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@ -490,9 +490,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
[[package]]
name = "cc"
version = "1.2.23"
version = "1.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766"
checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362"
dependencies = [
"shlex",
]
@ -1367,9 +1367,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.4.10"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5"
checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633"
dependencies = [
"atomic-waker",
"bytes",
@ -1648,7 +1648,7 @@ dependencies = [
"http 1.3.1",
"hyper 1.6.0",
"hyper-util",
"rustls 0.23.27",
"rustls 0.23.26",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.2",
@ -1985,9 +1985,9 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libm"
version = "0.2.15"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
checksum = "c9627da5196e5d8ed0b0495e61e518847578da83483c37288316d9b2e03a7f72"
[[package]]
name = "libmimalloc-sys"
@ -3221,9 +3221,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.27"
version = "0.23.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321"
checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0"
dependencies = [
"once_cell",
"rustls-pki-types",
@ -3851,7 +3851,7 @@ version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
dependencies = [
"rustls 0.23.27",
"rustls 0.23.26",
"tokio",
]
@ -3934,8 +3934,7 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_write",
"winnow 0.7.10",
"winnow 0.7.7",
]
[[package]]
@ -4812,9 +4811,9 @@ dependencies = [
[[package]]
name = "winnow"
version = "0.7.10"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5"
dependencies = [
"memchr",
]

View file

@ -176,6 +176,7 @@ rpassword = "7.4.0"
# Loading a dynamic CSS Stylesheet
grass_compiler = { version = "0.13.4", default-features = false }
# Strip debuginfo from the release builds
# The debug symbols are to provide better panic traces
# Also enable fat LTO and use 1 codegen unit for optimizations

View file

@ -336,7 +336,6 @@ async fn profile(headers: Headers, mut conn: DbConn) -> Json<Value> {
#[serde(rename_all = "camelCase")]
struct ProfileData {
// culture: String, // Ignored, always use en-US
// masterPasswordHint: Option<String>, // Ignored, has been moved to ChangePassData
name: String,
}

View file

@ -203,6 +203,7 @@ fn config() -> Json<Value> {
parse_experimental_client_feature_flags(&crate::CONFIG.experimental_client_feature_flags());
// Force the new key rotation feature
feature_states.insert("key-rotation-improvements".to_string(), true);
feature_states.insert("duo-redirect".to_string(), true);
feature_states.insert("flexible-collections-v-1".to_string(), false);
feature_states.insert("email-verification".to_string(), true);
@ -216,6 +217,7 @@ fn config() -> Json<Value> {
// - Individual cipher key encryption: 2024.2.0
"version": "2025.1.0",
"gitHash": option_env!("GIT_REV"),
"cloudRegion": null,
"server": {
"name": "Vaultwarden",
"url": "https://github.com/dani-garcia/vaultwarden"
@ -230,6 +232,11 @@ fn config() -> Json<Value> {
"notifications": format!("{domain}/notifications"),
"sso": "",
},
// Bitwarden uses this for the self-hosted servers to indicate the default push technology
"push": {
"pushTechnology": 0,
"vapidPublicKey": null
},
"featureStates": feature_states,
"object": "config",
}))

View file

@ -374,6 +374,21 @@ async fn get_org_collections_details(
|| (CONFIG.org_groups_enabled()
&& GroupUser::has_full_access_by_member(&org_id, &member.uuid, &mut conn).await);
// Get all admins, ownners and managers who can manage/access all
// Those are currently not listed in the col_users but need to be listed too.
let manage_all_members: Vec<Value> = Membership::find_confirmed_and_manage_all_by_org(&org_id, &mut conn)
.await
.into_iter()
.map(|member| {
json!({
"id": member.uuid,
"readOnly": false,
"hidePasswords": false,
"manage": true,
})
})
.collect();
for col in Collection::find_by_organization(&org_id, &mut conn).await {
// check whether the current user has access to the given collection
let assigned = has_full_access_to_org
@ -382,7 +397,7 @@ async fn get_org_collections_details(
&& GroupUser::has_access_to_collection_by_member(&col.uuid, &member.uuid, &mut conn).await);
// get the users assigned directly to the given collection
let users: Vec<Value> = col_users
let mut users: Vec<Value> = col_users
.iter()
.filter(|collection_member| collection_member.collection_uuid == col.uuid)
.map(|collection_member| {
@ -391,6 +406,7 @@ async fn get_org_collections_details(
)
})
.collect();
users.extend_from_slice(&manage_all_members);
// get the group details for the given collection
let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
@ -2556,18 +2572,27 @@ async fn _restore_member(
Ok(())
}
#[get("/organizations/<org_id>/groups")]
async fn get_groups(org_id: OrganizationId, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
async fn get_groups_data(
details: bool,
org_id: OrganizationId,
headers: ManagerHeadersLoose,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.membership.org_uuid {
err!("Organization not found", "Organization id's do not match");
}
let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
// Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::<Value>()
let groups = Group::find_by_organization(&org_id, &mut conn).await;
let mut groups_json = Vec::with_capacity(groups.len());
for g in groups {
groups_json.push(g.to_json_details(&mut conn).await)
if details {
for g in groups {
groups_json.push(g.to_json_details(&mut conn).await)
}
} else {
for g in groups {
groups_json.push(g.to_json())
}
}
groups_json
} else {
@ -2583,9 +2608,14 @@ async fn get_groups(org_id: OrganizationId, headers: ManagerHeadersLoose, mut co
})))
}
#[get("/organizations/<org_id>/groups")]
async fn get_groups(org_id: OrganizationId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {
get_groups_data(false, org_id, headers, conn).await
}
#[get("/organizations/<org_id>/groups/details", rank = 1)]
async fn get_groups_details(org_id: OrganizationId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {
get_groups(org_id, headers, conn).await
get_groups_data(true, org_id, headers, conn).await
}
#[derive(Deserialize)]
@ -2740,7 +2770,8 @@ async fn add_update_group(
"organizationId": group.organizations_uuid,
"name": group.name,
"accessAll": group.access_all,
"externalId": group.external_id
"externalId": group.external_id,
"object": "group"
})))
}

View file

@ -117,7 +117,7 @@ async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
// ---
// let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec, data.client_id);
device.save(conn).await?;
let result = json!({
@ -297,7 +297,7 @@ async fn _password_login(
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
// ---
// let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec, data.client_id);
device.save(conn).await?;
// Fetch all valid Master Password Policies and merge them into one with all true's and larges numbers as one policy
@ -312,6 +312,7 @@ async fn _password_login(
.filter_map(|p| serde_json::from_str(&p.data).ok())
.collect();
// NOTE: Upstream still uses PascalCase here for `Object`!
let master_password_policy = if !master_password_policies.is_empty() {
let mut mpp_json = json!(master_password_policies.into_iter().reduce(|acc, policy| {
MasterPasswordPolicy {
@ -324,10 +325,10 @@ async fn _password_login(
enforce_on_login: acc.enforce_on_login || policy.enforce_on_login,
}
}));
mpp_json["object"] = json!("masterPasswordPolicy");
mpp_json["Object"] = json!("masterPasswordPolicy");
mpp_json
} else {
json!({"object": "masterPasswordPolicy"})
json!({"Object": "masterPasswordPolicy"})
};
let mut result = json!({
@ -447,7 +448,7 @@ async fn _user_api_key_login(
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
// ---
// let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec, data.client_id);
device.save(conn).await?;
info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip);

View file

@ -181,6 +181,11 @@ pub struct LoginJwtClaims {
pub sstamp: String,
// device uuid
pub device: DeviceId,
// what kind of device, like FirefoxBrowser or Android derived from DeviceType
pub devicetype: String,
// the type of client_id, like web, cli, desktop, browser or mobile
pub client_id: String,
// [ "api", "offline_access" ]
pub scope: Vec<String>,
// [ "Application" ]

View file

@ -54,7 +54,7 @@ impl Device {
"id": self.uuid,
"name": self.name,
"type": self.atype,
"identifier": self.push_uuid,
"identifier": self.uuid,
"creationDate": format_date(&self.created_at),
"isTrusted": false,
"object":"device"
@ -73,7 +73,12 @@ impl Device {
self.twofactor_remember = None;
}
pub fn refresh_tokens(&mut self, user: &super::User, scope: Vec<String>) -> (String, i64) {
pub fn refresh_tokens(
&mut self,
user: &super::User,
scope: Vec<String>,
client_id: Option<String>,
) -> (String, i64) {
// If there is no refresh token, we create one
if self.refresh_token.is_empty() {
use data_encoding::BASE64URL;
@ -121,6 +126,8 @@ impl Device {
// orgmanager,
sstamp: user.security_stamp.clone(),
device: self.uuid.clone(),
devicetype: DeviceType::from_i32(self.atype).to_string(),
client_id: client_id.unwrap_or("undefined".to_string()),
scope,
amr: vec!["Application".into()],
};
@ -156,7 +163,7 @@ impl DeviceWithAuthRequest {
"id": self.device.uuid,
"name": self.device.name,
"type": self.device.atype,
"identifier": self.device.push_uuid,
"identifier": self.device.uuid,
"creationDate": format_date(&self.device.created_at),
"devicePendingAuthRequest": auth_request,
"isTrusted": false,

View file

@ -68,16 +68,11 @@ impl Group {
}
pub fn to_json(&self) -> Value {
use crate::util::format_date;
json!({
"id": self.uuid,
"organizationId": self.organizations_uuid,
"name": self.name,
"accessAll": self.access_all,
"externalId": self.external_id,
"creationDate": format_date(&self.creation_date),
"revisionDate": format_date(&self.revision_date),
"object": "group"
})
}

View file

@ -451,6 +451,8 @@ impl Membership {
"usePasswordManager": true,
"useCustomPermissions": true,
"useActivateAutofillPolicy": false,
"useAdminSponsoredFamilies": false,
"useRiskInsights": false, // Not supported (Not AGPLv3 Licensed)
"organizationUserId": self.uuid,
"providerId": null,
@ -458,7 +460,6 @@ impl Membership {
"providerType": null,
"familySponsorshipFriendlyName": null,
"familySponsorshipAvailable": false,
"planProductType": 3,
"productTierType": 3, // Enterprise tier
"keyConnectorEnabled": false,
"keyConnectorUrl": null,
@ -469,8 +470,10 @@ impl Membership {
"limitCollectionCreation": self.atype < MembershipType::Manager, // If less then a manager return true, to limit collection creations
"limitCollectionCreationDeletion": true,
"limitCollectionDeletion": true,
"limitItemDeletion": false,
"allowAdminAccessToAllCollectionItems": true,
"userIsManagedByOrganization": false, // Means not managed via the Members UI, like SSO
"userIsClaimedByOrganization": false, // The new key instead of the obsolete userIsManagedByOrganization
"permissions": permissions,
@ -616,6 +619,8 @@ impl Membership {
"permissions": permissions,
"ssoBound": false, // Not supported
"managedByOrganization": false, // This key is obsolete replaced by claimedByOrganization
"claimedByOrganization": false, // Means not managed via the Members UI, like SSO
"usesKeyConnector": false, // Not supported
"accessSecretsManager": false, // Not supported (Not AGPLv3 Licensed)
@ -863,6 +868,21 @@ impl Membership {
}}
}
// Get all users which are either owner or admin, or a manager which can manage/access all
pub async fn find_confirmed_and_manage_all_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter(
users_organizations::atype.eq_any(vec![MembershipType::Owner as i32, MembershipType::Admin as i32])
.or(users_organizations::atype.eq(MembershipType::Manager as i32).and(users_organizations::access_all.eq(true)))
)
.load::<MembershipDb>(conn)
.unwrap_or_default().from_db()
}}
}
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
db_run! { conn: {
users_organizations::table

View file

@ -249,7 +249,6 @@ impl User {
"emailVerified": !CONFIG.mail_enabled() || self.verified_at.is_some(),
"premium": true,
"premiumFromOrganization": false,
"masterPasswordHint": self.password_hint,
"culture": "en-US",
"twoFactorEnabled": twofactor_enabled,
"key": self.akey,