diff --git a/Cargo.lock b/Cargo.lock index d6f8b1db..051f2656 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index 119ecd1e..9d50d214 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 0b2266cc..761ccdeb 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -336,7 +336,6 @@ async fn profile(headers: Headers, mut conn: DbConn) -> Json { #[serde(rename_all = "camelCase")] struct ProfileData { // culture: String, // Ignored, always use en-US - // masterPasswordHint: Option, // Ignored, has been moved to ChangePassData name: String, } diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 3aa9ad79..5b38c24d 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -203,6 +203,7 @@ fn config() -> Json { 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 { // - 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 { "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", })) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 6ed0c127..88a72541 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -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 = 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 = col_users + let mut users: Vec = 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 = if CONFIG.org_groups_enabled() { @@ -2556,18 +2572,27 @@ async fn _restore_member( Ok(()) } -#[get("/organizations//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 = if CONFIG.org_groups_enabled() { - // Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::() 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//groups")] +async fn get_groups(org_id: OrganizationId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult { + get_groups_data(false, org_id, headers, conn).await +} + #[get("/organizations//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" }))) } diff --git a/src/api/identity.rs b/src/api/identity.rs index 90d356ee..0c1f70b0 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -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); diff --git a/src/auth.rs b/src/auth.rs index 939324d8..062db86c 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -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, // [ "Application" ] diff --git a/src/db/models/device.rs b/src/db/models/device.rs index b12bf70c..4a28480e 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -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, i64) { + pub fn refresh_tokens( + &mut self, + user: &super::User, + scope: Vec, + client_id: Option, + ) -> (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, diff --git a/src/db/models/group.rs b/src/db/models/group.rs index ebb4c31b..b9f91171 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -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" }) } diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index d62117c3..547726ca 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -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 { + 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::(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 diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 3c0c7857..b5b78ad0 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -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,