diff --git a/.env.template b/.env.template index 9d63fcff..f379fac6 100644 --- a/.env.template +++ b/.env.template @@ -344,21 +344,17 @@ ## Client Settings ## Enable experimental feature flags for clients. ## This is a comma-separated list of flags, e.g. "flag1,flag2,flag3". +## Note that clients cache the /api/config endpoint for about 1 hour and it could take some time before they are enabled or disabled! ## ## The following flags are available: -## - "autofill-overlay": Add an overlay menu to form fields for quick access to credentials. -## - "autofill-v2": Use the new autofill implementation. -## - "browser-fileless-import": Directly import credentials from other providers without a file. -## - "extension-refresh": Temporarily enable the new extension design until general availability (should be used with the beta Chrome extension) -## - "fido2-vault-credentials": Enable the use of FIDO2 security keys as second factor. ## - "inline-menu-positioning-improvements": Enable the use of inline menu password generator and identity suggestions in the browser extension. -## - "ssh-key-vault-item": Enable the creation and use of SSH key vault items. (Needs clients >=2024.12.0) +## - "inline-menu-totp": Enable the use of inline menu TOTP codes in the browser extension. ## - "ssh-agent": Enable SSH agent support on Desktop. (Needs desktop >=2024.12.0) +## - "ssh-key-vault-item": Enable the creation and use of SSH key vault items. (Needs clients >=2024.12.0) +## - "export-attachments": Enable support for exporting attachments (Clients >=2025.4.0) ## - "anon-addy-self-host-alias": Enable configuring self-hosted Anon Addy alias generator. (Needs Android >=2025.3.0, iOS >=2025.4.0) ## - "simple-login-self-host-alias": Enable configuring self-hosted Simple Login alias generator. (Needs Android >=2025.3.0, iOS >=2025.4.0) ## - "mutual-tls": Enable the use of mutual TLS on Android (Client >= 2025.2.0) -## - "export-attachments": Enable support for exporting attachments (Clients >=2025.4.0) -## - "inline-menu-totp": Enable the use of inline menu TOTP codes in the browser extension. # EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials ## Require new device emails. When a user logs in an email is required to be sent. diff --git a/Cargo.lock b/Cargo.lock index d6f8b1db..7d8ee10f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -154,9 +154,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +checksum = "1237c0ae75a0f3765f58910ff9cdd0a12eeb39ab2f4c7de23262f337f0aacbb3" dependencies = [ "async-lock", "cfg-if", @@ -165,7 +165,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 0.38.44", + "rustix 1.0.7", "slab", "tracing", "windows-sys 0.59.0", @@ -490,9 +490,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" [[package]] name = "cc" -version = "1.2.23" +version = "1.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" +checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" dependencies = [ "shlex", ] @@ -1440,12 +1440,6 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" -[[package]] -name = "hermit-abi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" - [[package]] name = "hermit-abi" version = "0.5.1" @@ -1640,11 +1634,10 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" dependencies = [ - "futures-util", "http 1.3.1", "hyper 1.6.0", "hyper-util", @@ -1673,9 +1666,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710" dependencies = [ "bytes", "futures-channel", @@ -1764,9 +1757,9 @@ checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", @@ -1780,9 +1773,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" @@ -2157,13 +2150,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2659,15 +2652,15 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "polling" -version = "3.7.4" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.4.0", + "hermit-abi 0.5.1", "pin-project-lite", - "rustix 0.38.44", + "rustix 1.0.7", "tracing", "windows-sys 0.59.0", ] @@ -3282,9 +3275,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "ryu" @@ -3524,9 +3517,9 @@ checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "socket2" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", @@ -3798,9 +3791,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.45.0" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", @@ -4145,11 +4138,13 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", ] [[package]] @@ -4482,15 +4477,15 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.61.1" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46ec44dc15085cea82cf9c78f85a9114c463a369786585ad2882d1ff0b0acf40" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", "windows-link", "windows-result", - "windows-strings 0.4.1", + "windows-strings 0.4.2", ] [[package]] @@ -4555,9 +4550,9 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] @@ -4573,9 +4568,9 @@ dependencies = [ [[package]] name = "windows-strings" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link", ] diff --git a/Cargo.toml b/Cargo.toml index 119ecd1e..2742f428 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ dashmap = "6.1.0" # Async futures futures = "0.3.31" -tokio = { version = "1.45.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } +tokio = { version = "1.45.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } # A generic serialization/deserialization framework serde = { version = "1.0.219", features = ["derive"] } @@ -95,7 +95,7 @@ ring = "0.17.14" subtle = "2.6.1" # UUID generation -uuid = { version = "1.16.0", features = ["v4"] } +uuid = { version = "1.17.0", features = ["v4"] } # Date and time libraries chrono = { version = "0.4.41", features = ["clock", "serde"], default-features = false } diff --git a/src/api/admin.rs b/src/api/admin.rs index f840fc6f..5034884f 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -421,11 +421,11 @@ async fn delete_user(user_id: UserId, token: AdminToken, mut conn: DbConn) -> Em async fn deauth_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { let mut user = get_user_or_404(&user_id, &mut conn).await?; - nt.send_logout(&user, None).await; + nt.send_logout(&user, None, &mut conn).await; if CONFIG.push_enabled() { for device in Device::find_push_devices_by_user(&user.uuid, &mut conn).await { - match unregister_push_device(device.push_uuid).await { + match unregister_push_device(&device.push_uuid).await { Ok(r) => r, Err(e) => error!("Unable to unregister devices from Bitwarden server: {e}"), }; @@ -447,7 +447,7 @@ async fn disable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt: let save_result = user.save(&mut conn).await; - nt.send_logout(&user, None).await; + nt.send_logout(&user, None, &mut conn).await; save_result } diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 0b2266cc..35d177d3 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, } @@ -462,7 +461,7 @@ async fn post_password(data: Json, headers: Headers, mut conn: D // Prevent logging out the client where the user requested this endpoint from. // If you do logout the user it will causes issues at the client side. // Adding the device uuid will prevent this. - nt.send_logout(&user, Some(headers.device.uuid.clone())).await; + nt.send_logout(&user, Some(headers.device.uuid.clone()), &mut conn).await; save_result } @@ -522,7 +521,7 @@ async fn post_kdf(data: Json, headers: Headers, mut conn: DbConn, user.set_password(&data.new_master_password_hash, Some(data.key), true, None); let save_result = user.save(&mut conn).await; - nt.send_logout(&user, Some(headers.device.uuid.clone())).await; + nt.send_logout(&user, Some(headers.device.uuid.clone()), &mut conn).await; save_result } @@ -734,7 +733,7 @@ async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, // Prevent logging out the client where the user requested this endpoint from. // If you do logout the user it will causes issues at the client side. // Adding the device uuid will prevent this. - nt.send_logout(&user, Some(headers.device.uuid.clone())).await; + nt.send_logout(&user, Some(headers.device.uuid.clone()), &mut conn).await; save_result } @@ -750,7 +749,7 @@ async fn post_sstamp(data: Json, headers: Headers, mut conn: user.reset_security_stamp(); let save_result = user.save(&mut conn).await; - nt.send_logout(&user, None).await; + nt.send_logout(&user, None, &mut conn).await; save_result } @@ -776,6 +775,11 @@ async fn post_email_token(data: Json, headers: Headers, mut conn } if User::find_by_mail(&data.new_email, &mut conn).await.is_some() { + if CONFIG.mail_enabled() { + if let Err(e) = mail::send_change_email_existing(&data.new_email, &user.email).await { + error!("Error sending change-email-existing email: {e:#?}"); + } + } err!("Email already in use"); } @@ -858,7 +862,7 @@ async fn post_email(data: Json, headers: Headers, mut conn: DbC let save_result = user.save(&mut conn).await; - nt.send_logout(&user, None).await; + nt.send_logout(&user, None, &mut conn).await; save_result } @@ -1056,7 +1060,7 @@ pub async fn _prelogin(data: Json, mut conn: DbConn) -> Json/clear-token")] async fn put_clear_device_token(device_id: DeviceId, mut conn: DbConn) -> EmptyResult { // This only clears push token - // https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109 - // https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37 + // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Controllers/DevicesController.cs#L215 + // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Services/Implementations/DeviceService.cs#L37 // This is somehow not implemented in any app, added it in case it is required + // 2025: Also, it looks like it only clears the first found device upstream, which is probably faulty. + // This because currently multiple accounts could be on the same device/app and that would cause issues. + // Vaultwarden removes the push-token for all devices, but this probably means we should also unregister all these devices. if !CONFIG.push_enabled() { return Ok(()); } if let Some(device) = Device::find_by_uuid(&device_id, &mut conn).await { Device::clear_push_token_by_uuid(&device_id, &mut conn).await?; - unregister_push_device(device.push_uuid).await?; + unregister_push_device(&device.push_uuid).await?; } Ok(()) @@ -1270,10 +1272,10 @@ async fn post_auth_request( }; // Validate device uuid and type - match Device::find_by_uuid_and_user(&data.device_identifier, &user.uuid, &mut conn).await { - Some(device) if device.atype == client_headers.device_type => {} + let device = match Device::find_by_uuid_and_user(&data.device_identifier, &user.uuid, &mut conn).await { + Some(device) if device.atype == client_headers.device_type => device, _ => err!("AuthRequest doesn't exist", "Device verification failed"), - } + }; let mut auth_request = AuthRequest::new( user.uuid.clone(), @@ -1285,7 +1287,7 @@ async fn post_auth_request( ); auth_request.save(&mut conn).await?; - nt.send_auth_request(&user.uuid, &auth_request.uuid, &data.device_identifier, &mut conn).await; + nt.send_auth_request(&user.uuid, &auth_request.uuid, &device, &mut conn).await; log_user_event( EventType::UserRequestedDeviceApproval as i32, @@ -1360,6 +1362,10 @@ async fn put_auth_request( err!("AuthRequest doesn't exist", "Record not found or user uuid does not match") }; + if headers.device.uuid != data.device_identifier { + err!("AuthRequest doesn't exist", "Device verification failed") + } + if auth_request.approved.is_some() { err!("An authentication request with the same device already exists") } @@ -1376,7 +1382,7 @@ async fn put_auth_request( auth_request.save(&mut conn).await?; ant.send_auth_response(&auth_request.user_uuid, &auth_request.uuid).await; - nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, &data.device_identifier, &mut conn).await; + nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, &headers.device, &mut conn).await; log_user_event( EventType::OrganizationUserApprovedAuthRequest as i32, diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index ad024076..9e7dc045 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -535,7 +535,7 @@ pub async fn update_cipher_from_data( ut, cipher, &cipher.update_users_revision(conn).await, - &headers.device.uuid, + &headers.device, shared_to_collections, conn, ) @@ -612,7 +612,7 @@ async fn post_ciphers_import( let mut user = headers.user; user.update_revision(&mut conn).await?; - nt.send_user_update(UpdateType::SyncVault, &user).await; + nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &mut conn).await; Ok(()) } @@ -808,7 +808,7 @@ async fn post_collections_update( UpdateType::SyncCipherUpdate, &cipher, &cipher.update_users_revision(&mut conn).await, - &headers.device.uuid, + &headers.device, Some(Vec::from_iter(posted_collections)), &mut conn, ) @@ -885,7 +885,7 @@ async fn post_collections_admin( UpdateType::SyncCipherUpdate, &cipher, &cipher.update_users_revision(&mut conn).await, - &headers.device.uuid, + &headers.device, Some(Vec::from_iter(posted_collections)), &mut conn, ) @@ -1281,7 +1281,7 @@ async fn save_attachment( UpdateType::SyncCipherUpdate, &cipher, &cipher.update_users_revision(&mut conn).await, - &headers.device.uuid, + &headers.device, None, &mut conn, ) @@ -1582,7 +1582,7 @@ async fn move_cipher_selected( UpdateType::SyncCipherUpdate, &cipher, std::slice::from_ref(&user_id), - &headers.device.uuid, + &headers.device, None, &mut conn, ) @@ -1629,7 +1629,7 @@ async fn delete_all( Some(member) => { if member.atype == MembershipType::Owner { Cipher::delete_all_by_organization(&org_data.org_id, &mut conn).await?; - nt.send_user_update(UpdateType::SyncVault, &user).await; + nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &mut conn).await; log_event( EventType::OrganizationPurgedVault as i32, @@ -1662,7 +1662,7 @@ async fn delete_all( } user.update_revision(&mut conn).await?; - nt.send_user_update(UpdateType::SyncVault, &user).await; + nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &mut conn).await; Ok(()) } @@ -1691,7 +1691,7 @@ async fn _delete_cipher_by_uuid( UpdateType::SyncCipherUpdate, &cipher, &cipher.update_users_revision(conn).await, - &headers.device.uuid, + &headers.device, None, conn, ) @@ -1702,7 +1702,7 @@ async fn _delete_cipher_by_uuid( UpdateType::SyncCipherDelete, &cipher, &cipher.update_users_revision(conn).await, - &headers.device.uuid, + &headers.device, None, conn, ) @@ -1767,7 +1767,7 @@ async fn _restore_cipher_by_uuid( UpdateType::SyncCipherUpdate, &cipher, &cipher.update_users_revision(conn).await, - &headers.device.uuid, + &headers.device, None, conn, ) @@ -1841,7 +1841,7 @@ async fn _delete_cipher_attachment_by_id( UpdateType::SyncCipherUpdate, &cipher, &cipher.update_users_revision(conn).await, - &headers.device.uuid, + &headers.device, None, conn, ) diff --git a/src/api/core/events.rs b/src/api/core/events.rs index 3a7d41f0..597c6ad6 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -29,7 +29,7 @@ struct EventRange { continuation_token: Option, } -// Upstream: https://github.com/bitwarden/server/blob/9ecf69d9cabce732cf2c57976dd9afa5728578fb/src/Api/Controllers/EventsController.cs#LL84C35-L84C41 +// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Controllers/EventsController.cs#L87 #[get("/organizations//events?")] async fn get_org_events( org_id: OrganizationId, @@ -169,8 +169,8 @@ struct EventCollection { } // Upstream: -// https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Events/Controllers/CollectController.cs -// https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Services/Implementations/EventService.cs +// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Events/Controllers/CollectController.cs +// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/EventService.cs #[post("/collect", format = "application/json", data = "")] async fn post_events_collect(data: Json>, headers: Headers, mut conn: DbConn) -> EmptyResult { if !CONFIG.org_events_enabled() { diff --git a/src/api/core/folders.rs b/src/api/core/folders.rs index 01dea4bb..c0769dad 100644 --- a/src/api/core/folders.rs +++ b/src/api/core/folders.rs @@ -45,7 +45,7 @@ async fn post_folders(data: Json, headers: Headers, mut conn: DbConn let mut folder = Folder::new(headers.user.uuid, data.name); folder.save(&mut conn).await?; - nt.send_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device.uuid, &mut conn).await; + nt.send_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device, &mut conn).await; Ok(Json(folder.to_json())) } @@ -78,7 +78,7 @@ async fn put_folder( folder.name = data.name; folder.save(&mut conn).await?; - nt.send_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device.uuid, &mut conn).await; + nt.send_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device, &mut conn).await; Ok(Json(folder.to_json())) } @@ -97,6 +97,6 @@ async fn delete_folder(folder_id: FolderId, headers: Headers, mut conn: DbConn, // Delete the actual folder entry folder.delete(&mut conn).await?; - nt.send_folder_update(UpdateType::SyncFolderDelete, &folder, &headers.device.uuid, &mut conn).await; + nt.send_folder_update(UpdateType::SyncFolderDelete, &folder, &headers.device, &mut conn).await; Ok(()) } diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 3aa9ad79..51c49cf6 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -124,7 +124,7 @@ async fn post_eq_domains( user.save(&mut conn).await?; - nt.send_user_update(UpdateType::SyncSettings, &user).await; + nt.send_user_update(UpdateType::SyncSettings, &user, &headers.device.push_uuid, &mut conn).await; Ok(Json(json!({}))) } @@ -199,12 +199,14 @@ fn get_api_webauthn(_headers: Headers) -> Json { #[get("/config")] fn config() -> Json { let domain = crate::CONFIG.domain(); + // Official available feature flags can be found here: + // Server (v2025.5.0): https://github.com/bitwarden/server/blob/4a7db112a0952c6df8bacf36c317e9c4e58c3651/src/Core/Constants.cs#L102 + // Client (v2025.5.0): https://github.com/bitwarden/clients/blob/9df8a3cc50ed45f52513e62c23fcc8a4b745f078/libs/common/src/enums/feature-flag.enum.ts#L10 + // Android (v2025.4.0): https://github.com/bitwarden/android/blob/bee09de972c3870de0d54a0067996be473ec55c7/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt#L27 + // iOS (v2025.4.0): https://github.com/bitwarden/ios/blob/956e05db67344c912e3a1b8cb2609165d67da1c9/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7 let mut feature_states = 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("flexible-collections-v-1".to_string(), false); - + feature_states.insert("duo-redirect".to_string(), true); feature_states.insert("email-verification".to_string(), true); feature_states.insert("unauth-ui-refresh".to_string(), true); @@ -214,7 +216,7 @@ fn config() -> Json { // We should make sure that we keep this updated when we support the new server features // Version history: // - Individual cipher key encryption: 2024.2.0 - "version": "2025.1.0", + "version": "2025.4.0", "gitHash": option_env!("GIT_REV"), "server": { "name": "Vaultwarden", @@ -229,6 +231,12 @@ fn config() -> Json { "identity": format!("{domain}/identity"), "notifications": format!("{domain}/notifications"), "sso": "", + "cloudRegion": null, + }, + // 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..2c70bff0 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, owners 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() { @@ -681,6 +697,9 @@ async fn _delete_organization_collection( headers: &ManagerHeaders, conn: &mut DbConn, ) -> EmptyResult { + if org_id != &headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let Some(collection) = Collection::find_by_uuid_and_org(col_id, org_id, conn).await else { err!("Collection not found", "Collection does not exist or does not belong to this organization") }; @@ -893,7 +912,7 @@ struct OrgIdData { #[get("/ciphers/organization-details?")] async fn get_org_details(data: OrgIdData, headers: OrgMemberHeaders, mut conn: DbConn) -> JsonResult { - if data.organization_id != headers.org_id { + if data.organization_id != headers.membership.org_uuid { err_code!("Resource not found.", "Organization id's do not match", rocket::http::Status::NotFound.code); } @@ -1180,6 +1199,9 @@ async fn reinvite_member( headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } _reinvite_member(&org_id, &member_id, &headers.user.email, &mut conn).await } @@ -1397,6 +1419,9 @@ async fn _confirm_invite( conn: &mut DbConn, nt: &Notify<'_>, ) -> EmptyResult { + if org_id != &headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } if key.is_empty() || member_id.is_empty() { err!("Key or UserId is not set, unable to process request"); } @@ -1460,7 +1485,7 @@ async fn _confirm_invite( let save_result = member_to_confirm.save(conn).await; if let Some(user) = User::find_by_uuid(&member_to_confirm.user_uuid, conn).await { - nt.send_user_update(UpdateType::SyncOrgKeys, &user).await; + nt.send_user_update(UpdateType::SyncOrgKeys, &user, &headers.device.push_uuid, conn).await; } save_result @@ -1719,6 +1744,9 @@ async fn _delete_member( conn: &mut DbConn, nt: &Notify<'_>, ) -> EmptyResult { + if org_id != &headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let Some(member_to_delete) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else { err!("User to delete isn't member of the organization") }; @@ -1747,7 +1775,7 @@ async fn _delete_member( .await; if let Some(user) = User::find_by_uuid(&member_to_delete.user_uuid, conn).await { - nt.send_user_update(UpdateType::SyncOrgKeys, &user).await; + nt.send_user_update(UpdateType::SyncOrgKeys, &user, &headers.device.push_uuid, conn).await; } member_to_delete.delete(conn).await @@ -1813,16 +1841,20 @@ struct RelationsData { value: usize, } +// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/ImportCiphersController.cs#L62 #[post("/ciphers/import-organization?", data = "")] async fn post_org_import( query: OrgIdData, data: Json, - headers: AdminHeaders, + headers: OrgMemberHeaders, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - let data: ImportData = data.into_inner(); let org_id = query.organization_id; + if org_id != headers.membership.org_uuid { + err!("Organization not found", "Organization id's do not match"); + } + let data: ImportData = data.into_inner(); // Validate the import before continuing // Bitwarden does not process the import if there is one item invalid. @@ -1835,8 +1867,20 @@ async fn post_org_import( let mut collections: Vec = Vec::with_capacity(data.collections.len()); for col in data.collections { let collection_uuid = if existing_collections.contains(&col.id) { - col.id.unwrap() + let col_id = col.id.unwrap(); + // When not an Owner or Admin, check if the member is allowed to access the collection. + if headers.membership.atype < MembershipType::Admin + && !Collection::can_access_collection(&headers.membership, &col_id, &mut conn).await + { + err!(Compact, "The current user isn't allowed to manage this collection") + } + col_id } else { + // We do not allow users or managers which can not manage all collections to create new collections + // If there is any collection other than an existing import collection, abort the import. + if headers.membership.atype <= MembershipType::Manager && !headers.membership.has_full_access() { + err!(Compact, "The current user isn't allowed to create new collections") + } let new_collection = Collection::new(org_id.clone(), col.name, col.external_id); new_collection.save(&mut conn).await?; new_collection.uuid @@ -1859,7 +1903,17 @@ async fn post_org_import( // Always clear folder_id's via an organization import cipher_data.folder_id = None; let mut cipher = Cipher::new(cipher_data.r#type, cipher_data.name.clone()); - update_cipher_from_data(&mut cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None).await.ok(); + update_cipher_from_data( + &mut cipher, + cipher_data, + &headers, + Some(collections.clone()), + &mut conn, + &nt, + UpdateType::None, + ) + .await + .ok(); ciphers.push(cipher.uuid); } @@ -1890,12 +1944,6 @@ struct BulkCollectionsData { async fn post_bulk_collections(data: Json, headers: Headers, mut conn: DbConn) -> EmptyResult { let data: BulkCollectionsData = data.into_inner(); - // This feature does not seem to be active on all the clients - // To prevent future issues, add a check to block a call when this is set to true - if data.remove_collections { - err!("Bulk removing of collections is not yet implemented") - } - // Get all the collection available to the user in one query // Also filter based upon the provided collections let user_collections: HashMap = @@ -1924,8 +1972,16 @@ async fn post_bulk_collections(data: Json, headers: Headers // Do not abort the operation just ignore it, it could be a cipher was just deleted for example if let Some(cipher) = Cipher::find_by_uuid_and_org(cipher_id, &data.organization_id, &mut conn).await { if cipher.is_write_accessible_to_user(&headers.user.uuid, &mut conn).await { - for collection in &data.collection_ids { - CollectionCipher::save(&cipher.uuid, collection, &mut conn).await?; + // When selecting a specific collection from the left filter list, and use the bulk option, you can remove an item from that collection + // In these cases the client will call this endpoint twice, once for adding the new collections and a second for deleting. + if data.remove_collections { + for collection in &data.collection_ids { + CollectionCipher::delete(&cipher.uuid, collection, &mut conn).await?; + } + } else { + for collection in &data.collection_ids { + CollectionCipher::save(&cipher.uuid, collection, &mut conn).await?; + } } } }; @@ -2402,6 +2458,9 @@ async fn _revoke_member( headers: &AdminHeaders, conn: &mut DbConn, ) -> EmptyResult { + if org_id != &headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } match Membership::find_by_uuid_and_org(member_id, org_id, conn).await { Some(mut member) if member.status > MembershipStatus::Revoked as i32 => { if member.user_uuid == headers.user.uuid { @@ -2509,6 +2568,9 @@ async fn _restore_member( headers: &AdminHeaders, conn: &mut DbConn, ) -> EmptyResult { + if org_id != &headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } match Membership::find_by_uuid_and_org(member_id, org_id, conn).await { Some(mut member) if member.status < MembershipStatus::Accepted as i32 => { if member.user_uuid == headers.user.uuid { @@ -2556,18 +2618,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 +2654,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)] @@ -2647,6 +2723,9 @@ async fn post_groups( data: Json, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2676,6 +2755,9 @@ async fn put_group( headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2740,7 +2822,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" }))) } @@ -2791,6 +2874,9 @@ async fn _delete_group( headers: &AdminHeaders, conn: &mut DbConn, ) -> EmptyResult { + if org_id != &headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2820,6 +2906,9 @@ async fn bulk_delete_groups( headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2883,6 +2972,9 @@ async fn put_group_members( data: Json>, mut conn: DbConn, ) -> EmptyResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -3067,7 +3159,7 @@ async fn get_organization_public_key( headers: OrgMemberHeaders, mut conn: DbConn, ) -> JsonResult { - if org_id != headers.org_id { + if org_id != headers.membership.org_uuid { err!("Organization not found", "Organization id's do not match"); } let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else { @@ -3081,7 +3173,7 @@ async fn get_organization_public_key( } // Obsolete - Renamed to public-key (2023.8), left for backwards compatibility with older clients -// https://github.com/bitwarden/server/blob/25dc0c9178e3e3584074bbef0d4be827b7c89415/src/Api/AdminConsole/Controllers/OrganizationsController.cs#L463-L468 +// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Controllers/OrganizationsController.cs#L487-L492 #[get("/organizations//keys")] async fn get_organization_keys(org_id: OrganizationId, headers: OrgMemberHeaders, conn: DbConn) -> JsonResult { get_organization_public_key(org_id, headers, conn).await @@ -3132,7 +3224,7 @@ async fn put_reset_password( user.set_password(reset_request.new_master_password_hash.as_str(), Some(reset_request.key), true, None); user.save(&mut conn).await?; - nt.send_logout(&user, None).await; + nt.send_logout(&user, None, &mut conn).await; log_event( EventType::OrganizationUserAdminResetPassword as i32, @@ -3172,16 +3264,16 @@ async fn get_reset_password_details( check_reset_password_applicable_and_permissions(&org_id, &member_id, &headers, &mut conn).await?; - // https://github.com/bitwarden/server/blob/3b50ccb9f804efaacdc46bed5b60e5b28eddefcf/src/Api/Models/Response/Organizations/OrganizationUserResponseModel.cs#L111 + // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs#L190 Ok(Json(json!({ "object": "organizationUserResetPasswordDetails", - "kdf":user.client_kdf_type, - "kdfIterations":user.client_kdf_iter, - "kdfMemory":user.client_kdf_memory, - "kdfParallelism":user.client_kdf_parallelism, - "resetPasswordKey":member.reset_password_key, - "encryptedPrivateKey":org.private_key, - + "organizationUserId": member_id, + "kdf": user.client_kdf_type, + "kdfIterations": user.client_kdf_iter, + "kdfMemory": user.client_kdf_memory, + "kdfParallelism": user.client_kdf_parallelism, + "resetPasswordKey": member.reset_password_key, + "encryptedPrivateKey": org.private_key, }))) } @@ -3269,6 +3361,9 @@ async fn put_reset_password_enrollment( // NOTE: It seems clients can't handle uppercase-first keys!! // We need to convert all keys so they have the first character to be a lowercase. // Else the export will be just an empty JSON file. +// We currently only support exports by members of the Admin or Owner status. +// Vaultwarden does not yet support exporting only managed collections! +// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/OrganizationExportController.cs#L52 #[get("/organizations//export")] async fn get_org_export(org_id: OrganizationId, headers: AdminHeaders, mut conn: DbConn) -> JsonResult { if org_id != headers.org_id { @@ -3288,6 +3383,9 @@ async fn _api_key( headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != &headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let data: PasswordOrOtpData = data.into_inner(); let user = headers.user; diff --git a/src/api/core/public.rs b/src/api/core/public.rs index 1c85ae1b..84606de6 100644 --- a/src/api/core/public.rs +++ b/src/api/core/public.rs @@ -46,7 +46,7 @@ struct OrgImportData { #[post("/public/organization/import", data = "")] async fn ldap_import(data: Json, token: PublicToken, mut conn: DbConn) -> EmptyResult { // Most of the logic for this function can be found here - // https://github.com/bitwarden/server/blob/fd892b2ff4547648a276734fb2b14a8abae2c6f5/src/Core/Services/Implementations/OrganizationService.cs#L1797 + // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs#L1203 let org_id = token.0; let data = data.into_inner(); diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index 01bce86f..e99face4 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -2,6 +2,7 @@ use std::path::Path; use chrono::{DateTime, TimeDelta, Utc}; use num_traits::ToPrimitive; +use once_cell::sync::Lazy; use rocket::form::Form; use rocket::fs::NamedFile; use rocket::fs::TempFile; @@ -17,6 +18,21 @@ use crate::{ }; const SEND_INACCESSIBLE_MSG: &str = "Send does not exist or is no longer available"; +static ANON_PUSH_DEVICE: Lazy = Lazy::new(|| { + let dt = crate::util::parse_date("1970-01-01T00:00:00.000000Z"); + Device { + uuid: String::from("00000000-0000-0000-0000-000000000000").into(), + created_at: dt, + updated_at: dt, + user_uuid: String::from("00000000-0000-0000-0000-000000000000").into(), + name: String::new(), + atype: 14, // 14 == Unknown Browser + push_uuid: Some(String::from("00000000-0000-0000-0000-000000000000").into()), + push_token: None, + refresh_token: String::new(), + twofactor_remember: None, + } +}); // The max file size allowed by Bitwarden clients and add an extra 5% to avoid issues const SIZE_525_MB: i64 = 550_502_400; @@ -182,7 +198,7 @@ async fn post_send(data: Json, headers: Headers, mut conn: DbConn, nt: UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await, - &headers.device.uuid, + &headers.device, &mut conn, ) .await; @@ -204,6 +220,8 @@ struct UploadDataV2<'f> { // @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads (v2). // This method still exists to support older clients, probably need to remove it sometime. // Upstream: https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L164-L167 +// 2025: This endpoint doesn't seem to exists anymore in the latest version +// See: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs #[post("/sends/file", format = "multipart/form-data", data = "")] async fn post_send_file(data: Form>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { enforce_disable_send_policy(&headers, &mut conn).await?; @@ -272,7 +290,7 @@ async fn post_send_file(data: Form>, headers: Headers, mut conn: UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await, - &headers.device.uuid, + &headers.device, &mut conn, ) .await; @@ -280,7 +298,7 @@ async fn post_send_file(data: Form>, headers: Headers, mut conn: Ok(Json(send.to_json())) } -// Upstream: https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L190 +// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs#L165 #[post("/sends/file/v2", data = "")] async fn post_send_file_v2(data: Json, headers: Headers, mut conn: DbConn) -> JsonResult { enforce_disable_send_policy(&headers, &mut conn).await?; @@ -351,7 +369,7 @@ pub struct SendFileData { fileName: String, } -// https://github.com/bitwarden/server/blob/66f95d1c443490b653e5a15d32977e2f5a3f9e32/src/Api/Tools/Controllers/SendsController.cs#L250 +// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs#L195 #[post("/sends//file/", format = "multipart/form-data", data = "")] async fn post_send_file_v2_data( send_id: SendId, @@ -424,7 +442,7 @@ async fn post_send_file_v2_data( UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await, - &headers.device.uuid, + &headers.device, &mut conn, ) .await; @@ -489,7 +507,7 @@ async fn post_access( UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await, - &String::from("00000000-0000-0000-0000-000000000000").into(), + &ANON_PUSH_DEVICE, &mut conn, ) .await; @@ -546,7 +564,7 @@ async fn post_access_file( UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await, - &String::from("00000000-0000-0000-0000-000000000000").into(), + &ANON_PUSH_DEVICE, &mut conn, ) .await; @@ -645,7 +663,7 @@ pub async fn update_send_from_data( send.save(conn).await?; if ut != UpdateType::None { - nt.send_send_update(ut, send, &send.update_users_revision(conn).await, &headers.device.uuid, conn).await; + nt.send_send_update(ut, send, &send.update_users_revision(conn).await, &headers.device, conn).await; } Ok(()) } @@ -661,7 +679,7 @@ async fn delete_send(send_id: SendId, headers: Headers, mut conn: DbConn, nt: No UpdateType::SyncSendDelete, &send, &send.update_users_revision(&mut conn).await, - &headers.device.uuid, + &headers.device, &mut conn, ) .await; @@ -683,7 +701,7 @@ async fn put_remove_password(send_id: SendId, headers: Headers, mut conn: DbConn UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await, - &headers.device.uuid, + &headers.device, &mut conn, ) .await; diff --git a/src/api/core/two_factor/authenticator.rs b/src/api/core/two_factor/authenticator.rs index 3bdb1226..e5ffeedc 100644 --- a/src/api/core/two_factor/authenticator.rs +++ b/src/api/core/two_factor/authenticator.rs @@ -34,6 +34,10 @@ async fn generate_authenticator(data: Json, headers: Headers, _ => (false, crypto::encode_random_bytes::<20>(BASE32)), }; + // Upstream seems to also return `userVerificationToken`, but doesn't seem to be used at all. + // It should help prevent TOTP disclosure if someone keeps their vault unlocked. + // Since it doesn't seem to be used, and also does not cause any issues, lets leave it out of the response. + // See: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Auth/Controllers/TwoFactorController.cs#L94 Ok(Json(json!({ "enabled": enabled, "key": key, diff --git a/src/api/core/two_factor/duo.rs b/src/api/core/two_factor/duo.rs index 7f7d42ba..6600bfde 100644 --- a/src/api/core/two_factor/duo.rs +++ b/src/api/core/two_factor/duo.rs @@ -118,6 +118,9 @@ async fn get_duo(data: Json, headers: Headers, mut conn: DbCo } else { json!({ "enabled": enabled, + "host": null, + "clientSecret": null, + "clientId": null, "object": "twoFactorDuo" }) }; diff --git a/src/api/core/two_factor/duo_oidc.rs b/src/api/core/two_factor/duo_oidc.rs index e554b1d6..ad948a75 100644 --- a/src/api/core/two_factor/duo_oidc.rs +++ b/src/api/core/two_factor/duo_oidc.rs @@ -21,7 +21,7 @@ use url::Url; // The location on this service that Duo should redirect users to. For us, this is a bridge // built in to the Bitwarden clients. -// See: https://github.com/bitwarden/clients/blob/main/apps/web/src/connectors/duo-redirect.ts +// See: https://github.com/bitwarden/clients/blob/5fb46df3415aefced0b52f2db86c873962255448/apps/web/src/connectors/duo-redirect.ts const DUO_REDIRECT_LOCATION: &str = "duo-redirect-connector.html"; // Number of seconds that a JWT we generate for Duo should be valid for. diff --git a/src/api/identity.rs b/src/api/identity.rs index 90d356ee..f47a5e2c 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,10 +297,10 @@ 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 + // Fetch all valid Master Password Policies and merge them into one with all trues and largest numbers as one policy let master_password_policies: Vec = OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy( &user.uuid, @@ -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/api/notifications.rs b/src/api/notifications.rs index 2b325b70..ccf4c13c 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -10,7 +10,7 @@ use rocket_ws::{Message, WebSocket}; use crate::{ auth::{ClientIp, WsAccessTokenHeader}, db::{ - models::{AuthRequestId, Cipher, CollectionId, DeviceId, Folder, Send as DbSend, User, UserId}, + models::{AuthRequestId, Cipher, CollectionId, Device, DeviceId, Folder, PushId, Send as DbSend, User, UserId}, DbConn, }, Error, CONFIG, @@ -339,7 +339,7 @@ impl WebSocketUsers { } // NOTE: The last modified date needs to be updated before calling these methods - pub async fn send_user_update(&self, ut: UpdateType, user: &User) { + pub async fn send_user_update(&self, ut: UpdateType, user: &User, push_uuid: &Option, conn: &mut DbConn) { // Skip any processing if both WebSockets and Push are not active if *NOTIFICATIONS_DISABLED { return; @@ -355,11 +355,11 @@ impl WebSocketUsers { } if CONFIG.push_enabled() { - push_user_update(ut, user); + push_user_update(ut, user, push_uuid, conn).await; } } - pub async fn send_logout(&self, user: &User, acting_device_id: Option) { + pub async fn send_logout(&self, user: &User, acting_device_id: Option, conn: &mut DbConn) { // Skip any processing if both WebSockets and Push are not active if *NOTIFICATIONS_DISABLED { return; @@ -375,17 +375,11 @@ impl WebSocketUsers { } if CONFIG.push_enabled() { - push_logout(user, acting_device_id.clone()); + push_logout(user, acting_device_id.clone(), conn).await; } } - pub async fn send_folder_update( - &self, - ut: UpdateType, - folder: &Folder, - acting_device_id: &DeviceId, - conn: &mut DbConn, - ) { + pub async fn send_folder_update(&self, ut: UpdateType, folder: &Folder, device: &Device, conn: &mut DbConn) { // Skip any processing if both WebSockets and Push are not active if *NOTIFICATIONS_DISABLED { return; @@ -397,7 +391,7 @@ impl WebSocketUsers { ("RevisionDate".into(), serialize_date(folder.updated_at)), ], ut, - Some(acting_device_id.clone()), + Some(device.uuid.clone()), ); if CONFIG.enable_websocket() { @@ -405,7 +399,7 @@ impl WebSocketUsers { } if CONFIG.push_enabled() { - push_folder_update(ut, folder, acting_device_id, conn).await; + push_folder_update(ut, folder, device, conn).await; } } @@ -414,7 +408,7 @@ impl WebSocketUsers { ut: UpdateType, cipher: &Cipher, user_ids: &[UserId], - acting_device_id: &DeviceId, + device: &Device, collection_uuids: Option>, conn: &mut DbConn, ) { @@ -444,7 +438,7 @@ impl WebSocketUsers { ("RevisionDate".into(), revision_date), ], ut, - Some(acting_device_id.clone()), + Some(device.uuid.clone()), // Acting device id (unique device/app uuid) ); if CONFIG.enable_websocket() { @@ -454,7 +448,7 @@ impl WebSocketUsers { } if CONFIG.push_enabled() && user_ids.len() == 1 { - push_cipher_update(ut, cipher, acting_device_id, conn).await; + push_cipher_update(ut, cipher, device, conn).await; } } @@ -463,7 +457,7 @@ impl WebSocketUsers { ut: UpdateType, send: &DbSend, user_ids: &[UserId], - acting_device_id: &DeviceId, + device: &Device, conn: &mut DbConn, ) { // Skip any processing if both WebSockets and Push are not active @@ -488,7 +482,7 @@ impl WebSocketUsers { } } if CONFIG.push_enabled() && user_ids.len() == 1 { - push_send_update(ut, send, acting_device_id, conn).await; + push_send_update(ut, send, device, conn).await; } } @@ -496,7 +490,7 @@ impl WebSocketUsers { &self, user_id: &UserId, auth_request_uuid: &str, - acting_device_id: &DeviceId, + device: &Device, conn: &mut DbConn, ) { // Skip any processing if both WebSockets and Push are not active @@ -506,14 +500,14 @@ impl WebSocketUsers { let data = create_update( vec![("Id".into(), auth_request_uuid.to_owned().into()), ("UserId".into(), user_id.to_string().into())], UpdateType::AuthRequest, - Some(acting_device_id.clone()), + Some(device.uuid.clone()), ); if CONFIG.enable_websocket() { self.send_update(user_id, &data).await; } if CONFIG.push_enabled() { - push_auth_request(user_id.clone(), auth_request_uuid.to_owned(), conn).await; + push_auth_request(user_id, auth_request_uuid, device, conn).await; } } @@ -521,7 +515,7 @@ impl WebSocketUsers { &self, user_id: &UserId, auth_request_id: &AuthRequestId, - approving_device_id: &DeviceId, + device: &Device, conn: &mut DbConn, ) { // Skip any processing if both WebSockets and Push are not active @@ -531,14 +525,14 @@ impl WebSocketUsers { let data = create_update( vec![("Id".into(), auth_request_id.to_string().into()), ("UserId".into(), user_id.to_string().into())], UpdateType::AuthRequestResponse, - Some(approving_device_id.clone()), + Some(device.uuid.clone()), ); if CONFIG.enable_websocket() { self.send_update(user_id, &data).await; } if CONFIG.push_enabled() { - push_auth_response(user_id, auth_request_id, approving_device_id, conn).await; + push_auth_response(user_id, auth_request_id, device, conn).await; } } } diff --git a/src/api/push.rs b/src/api/push.rs index d97ef7b4..f3ade9b0 100644 --- a/src/api/push.rs +++ b/src/api/push.rs @@ -7,9 +7,9 @@ use tokio::sync::RwLock; use crate::{ api::{ApiResult, EmptyResult, UpdateType}, - db::models::{AuthRequestId, Cipher, Device, DeviceId, Folder, Send, User, UserId}, + db::models::{AuthRequestId, Cipher, Device, DeviceId, Folder, PushId, Send, User, UserId}, http_client::make_http_request, - util::format_date, + util::{format_date, get_uuid}, CONFIG, }; @@ -28,20 +28,20 @@ struct LocalAuthPushToken { valid_until: Instant, } -async fn get_auth_push_token() -> ApiResult { - static PUSH_TOKEN: Lazy> = Lazy::new(|| { +async fn get_auth_api_token() -> ApiResult { + static API_TOKEN: Lazy> = Lazy::new(|| { RwLock::new(LocalAuthPushToken { access_token: String::new(), valid_until: Instant::now(), }) }); - let push_token = PUSH_TOKEN.read().await; + let api_token = API_TOKEN.read().await; - if push_token.valid_until.saturating_duration_since(Instant::now()).as_secs() > 0 { + if api_token.valid_until.saturating_duration_since(Instant::now()).as_secs() > 0 { debug!("Auth Push token still valid, no need for a new one"); - return Ok(push_token.access_token.clone()); + return Ok(api_token.access_token.clone()); } - drop(push_token); // Drop the read lock now + drop(api_token); // Drop the read lock now let installation_id = CONFIG.push_installation_id(); let client_id = format!("installation.{installation_id}"); @@ -68,44 +68,48 @@ async fn get_auth_push_token() -> ApiResult { Err(e) => err!(format!("Unexpected push token received from bitwarden server: {e}")), }; - let mut push_token = PUSH_TOKEN.write().await; - push_token.valid_until = Instant::now() + let mut api_token = API_TOKEN.write().await; + api_token.valid_until = Instant::now() .checked_add(Duration::new((json_pushtoken.expires_in / 2) as u64, 0)) // Token valid for half the specified time .unwrap(); - push_token.access_token = json_pushtoken.access_token; + api_token.access_token = json_pushtoken.access_token; - debug!("Token still valid for {}", push_token.valid_until.saturating_duration_since(Instant::now()).as_secs()); - Ok(push_token.access_token.clone()) + debug!("Token still valid for {}", api_token.valid_until.saturating_duration_since(Instant::now()).as_secs()); + Ok(api_token.access_token.clone()) } pub async fn register_push_device(device: &mut Device, conn: &mut crate::db::DbConn) -> EmptyResult { - if !CONFIG.push_enabled() || !device.is_push_device() || device.is_registered() { + if !CONFIG.push_enabled() || !device.is_push_device() { return Ok(()); } if device.push_token.is_none() { - warn!("Skipping the registration of the device {} because the push_token field is empty.", device.uuid); - warn!("To get rid of this message you need to clear the app data and reconnect the device."); + warn!("Skipping the registration of the device {:?} because the push_token field is empty.", device.uuid); + warn!("To get rid of this message you need to logout, clear the app data and login again on the device."); return Ok(()); } - debug!("Registering Device {}", device.uuid); + debug!("Registering Device {:?}", device.push_uuid); - // generate a random push_uuid so we know the device is registered - device.push_uuid = Some(uuid::Uuid::new_v4().to_string()); + // Generate a random push_uuid so if it doesn't already have one + if device.push_uuid.is_none() { + device.push_uuid = Some(PushId(get_uuid())); + } //Needed to register a device for push to bitwarden : let data = json!({ + "deviceId": device.push_uuid, // Unique UUID per user/device + "pushToken": device.push_token, "userId": device.user_uuid, - "deviceId": device.push_uuid, - "identifier": device.uuid, "type": device.atype, - "pushToken": device.push_token + "identifier": device.uuid, // Unique UUID of the device/app, determined by the device/app it self currently registering + // "organizationIds:" [] // TODO: This is not yet implemented by Vaultwarden! + "installationId": CONFIG.push_installation_id(), }); - let auth_push_token = get_auth_push_token().await?; - let auth_header = format!("Bearer {}", &auth_push_token); + let auth_api_token = get_auth_api_token().await?; + let auth_header = format!("Bearer {auth_api_token}"); if let Err(e) = make_http_request(Method::POST, &(CONFIG.push_relay_uri() + "/push/register"))? .header(CONTENT_TYPE, "application/json") @@ -126,18 +130,21 @@ pub async fn register_push_device(device: &mut Device, conn: &mut crate::db::DbC Ok(()) } -pub async fn unregister_push_device(push_id: Option) -> EmptyResult { +pub async fn unregister_push_device(push_id: &Option) -> EmptyResult { if !CONFIG.push_enabled() || push_id.is_none() { return Ok(()); } - let auth_push_token = get_auth_push_token().await?; + let auth_api_token = get_auth_api_token().await?; - let auth_header = format!("Bearer {}", &auth_push_token); + let auth_header = format!("Bearer {auth_api_token}"); - match make_http_request(Method::DELETE, &(CONFIG.push_relay_uri() + "/push/" + &push_id.unwrap()))? - .header(AUTHORIZATION, auth_header) - .send() - .await + match make_http_request( + Method::POST, + &format!("{}/push/delete/{}", CONFIG.push_relay_uri(), push_id.as_ref().unwrap()), + )? + .header(AUTHORIZATION, auth_header) + .send() + .await { Ok(r) => r, Err(e) => err!(format!("An error occurred during device unregistration: {e}")), @@ -145,12 +152,7 @@ pub async fn unregister_push_device(push_id: Option) -> EmptyResult { Ok(()) } -pub async fn push_cipher_update( - ut: UpdateType, - cipher: &Cipher, - acting_device_id: &DeviceId, - conn: &mut crate::db::DbConn, -) { +pub async fn push_cipher_update(ut: UpdateType, cipher: &Cipher, device: &Device, conn: &mut crate::db::DbConn) { // We shouldn't send a push notification on cipher update if the cipher belongs to an organization, this isn't implemented in the upstream server too. if cipher.organization_uuid.is_some() { return; @@ -163,87 +165,97 @@ pub async fn push_cipher_update( if Device::check_user_has_push_device(user_id, conn).await { send_to_push_relay(json!({ "userId": user_id, - "organizationId": (), - "deviceId": acting_device_id, - "identifier": acting_device_id, + "organizationId": null, + "deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device) + "identifier": device.uuid, // Should be the acting device id (aka uuid per device/app) "type": ut as i32, "payload": { - "Id": cipher.uuid, - "UserId": cipher.user_uuid, - "OrganizationId": (), - "RevisionDate": format_date(&cipher.updated_at) - } + "id": cipher.uuid, + "userId": cipher.user_uuid, + "organizationId": null, + "collectionIds": null, + "revisionDate": format_date(&cipher.updated_at) + }, + "clientType": null, + "installationId": null })) .await; } } -pub fn push_logout(user: &User, acting_device_id: Option) { +pub async fn push_logout(user: &User, acting_device_id: Option, conn: &mut crate::db::DbConn) { let acting_device_id: Value = acting_device_id.map(|v| v.to_string().into()).unwrap_or_else(|| Value::Null); - tokio::task::spawn(send_to_push_relay(json!({ - "userId": user.uuid, - "organizationId": (), - "deviceId": acting_device_id, - "identifier": acting_device_id, - "type": UpdateType::LogOut as i32, - "payload": { - "UserId": user.uuid, - "Date": format_date(&user.updated_at) - } - }))); + if Device::check_user_has_push_device(&user.uuid, conn).await { + tokio::task::spawn(send_to_push_relay(json!({ + "userId": user.uuid, + "organizationId": (), + "deviceId": acting_device_id, + "identifier": acting_device_id, + "type": UpdateType::LogOut as i32, + "payload": { + "userId": user.uuid, + "date": format_date(&user.updated_at) + }, + "clientType": null, + "installationId": null + }))); + } } -pub fn push_user_update(ut: UpdateType, user: &User) { - tokio::task::spawn(send_to_push_relay(json!({ - "userId": user.uuid, - "organizationId": (), - "deviceId": (), - "identifier": (), - "type": ut as i32, - "payload": { - "UserId": user.uuid, - "Date": format_date(&user.updated_at) - } - }))); +pub async fn push_user_update(ut: UpdateType, user: &User, push_uuid: &Option, conn: &mut crate::db::DbConn) { + if Device::check_user_has_push_device(&user.uuid, conn).await { + tokio::task::spawn(send_to_push_relay(json!({ + "userId": user.uuid, + "organizationId": null, + "deviceId": push_uuid, + "identifier": null, + "type": ut as i32, + "payload": { + "userId": user.uuid, + "date": format_date(&user.updated_at) + }, + "clientType": null, + "installationId": null + }))); + } } -pub async fn push_folder_update( - ut: UpdateType, - folder: &Folder, - acting_device_id: &DeviceId, - conn: &mut crate::db::DbConn, -) { +pub async fn push_folder_update(ut: UpdateType, folder: &Folder, device: &Device, conn: &mut crate::db::DbConn) { if Device::check_user_has_push_device(&folder.user_uuid, conn).await { tokio::task::spawn(send_to_push_relay(json!({ "userId": folder.user_uuid, - "organizationId": (), - "deviceId": acting_device_id, - "identifier": acting_device_id, + "organizationId": null, + "deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device) + "identifier": device.uuid, // Should be the acting device id (aka uuid per device/app) "type": ut as i32, "payload": { - "Id": folder.uuid, - "UserId": folder.user_uuid, - "RevisionDate": format_date(&folder.updated_at) - } + "id": folder.uuid, + "userId": folder.user_uuid, + "revisionDate": format_date(&folder.updated_at) + }, + "clientType": null, + "installationId": null }))); } } -pub async fn push_send_update(ut: UpdateType, send: &Send, acting_device_id: &DeviceId, conn: &mut crate::db::DbConn) { +pub async fn push_send_update(ut: UpdateType, send: &Send, device: &Device, conn: &mut crate::db::DbConn) { if let Some(s) = &send.user_uuid { if Device::check_user_has_push_device(s, conn).await { tokio::task::spawn(send_to_push_relay(json!({ "userId": send.user_uuid, - "organizationId": (), - "deviceId": acting_device_id, - "identifier": acting_device_id, + "organizationId": null, + "deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device) + "identifier": device.uuid, // Should be the acting device id (aka uuid per device/app) "type": ut as i32, "payload": { - "Id": send.uuid, - "UserId": send.user_uuid, - "RevisionDate": format_date(&send.revision_date) - } + "id": send.uuid, + "userId": send.user_uuid, + "revisionDate": format_date(&send.revision_date) + }, + "clientType": null, + "installationId": null }))); } } @@ -254,7 +266,7 @@ async fn send_to_push_relay(notification_data: Value) { return; } - let auth_push_token = match get_auth_push_token().await { + let auth_api_token = match get_auth_api_token().await { Ok(s) => s, Err(e) => { debug!("Could not get the auth push token: {e}"); @@ -262,7 +274,7 @@ async fn send_to_push_relay(notification_data: Value) { } }; - let auth_header = format!("Bearer {}", &auth_push_token); + let auth_header = format!("Bearer {auth_api_token}"); let req = match make_http_request(Method::POST, &(CONFIG.push_relay_uri() + "/push/send")) { Ok(r) => r, @@ -284,18 +296,20 @@ async fn send_to_push_relay(notification_data: Value) { }; } -pub async fn push_auth_request(user_id: UserId, auth_request_id: String, conn: &mut crate::db::DbConn) { - if Device::check_user_has_push_device(&user_id, conn).await { +pub async fn push_auth_request(user_id: &UserId, auth_request_id: &str, device: &Device, conn: &mut crate::db::DbConn) { + if Device::check_user_has_push_device(user_id, conn).await { tokio::task::spawn(send_to_push_relay(json!({ "userId": user_id, - "organizationId": (), - "deviceId": null, - "identifier": null, + "organizationId": null, + "deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device) + "identifier": device.uuid, // Should be the acting device id (aka uuid per device/app) "type": UpdateType::AuthRequest as i32, "payload": { - "Id": auth_request_id, - "UserId": user_id, - } + "userId": user_id, + "id": auth_request_id, + }, + "clientType": null, + "installationId": null }))); } } @@ -303,20 +317,22 @@ pub async fn push_auth_request(user_id: UserId, auth_request_id: String, conn: & pub async fn push_auth_response( user_id: &UserId, auth_request_id: &AuthRequestId, - approving_device_id: &DeviceId, + device: &Device, conn: &mut crate::db::DbConn, ) { if Device::check_user_has_push_device(user_id, conn).await { tokio::task::spawn(send_to_push_relay(json!({ "userId": user_id, - "organizationId": (), - "deviceId": approving_device_id, - "identifier": approving_device_id, + "organizationId": null, + "deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device) + "identifier": device.uuid, // Should be the acting device id (aka uuid per device/app) "type": UpdateType::AuthRequestResponse as i32, "payload": { - "Id": auth_request_id, - "UserId": user_id, - } + "userId": user_id, + "id": auth_request_id, + }, + "clientType": null, + "installationId": null }))); } } diff --git a/src/auth.rs b/src/auth.rs index 939324d8..c86c0a41 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" ] @@ -689,17 +694,6 @@ impl<'r> FromRequest<'r> for AdminHeaders { } } -impl From for Headers { - fn from(h: AdminHeaders) -> Headers { - Headers { - host: h.host, - device: h.device, - user: h.user, - ip: h.ip, - } - } -} - // col_id is usually the fourth path param ("/organizations//collections/"), // but there could be cases where it is a query value. // First check the path, if this is not a valid uuid, try the query values. @@ -869,8 +863,10 @@ impl<'r> FromRequest<'r> for OwnerHeaders { pub struct OrgMemberHeaders { pub host: String, + pub device: Device, pub user: User, - pub org_id: OrganizationId, + pub membership: Membership, + pub ip: ClientIp, } #[rocket::async_trait] @@ -882,8 +878,10 @@ impl<'r> FromRequest<'r> for OrgMemberHeaders { if headers.is_member() { Outcome::Success(Self { host: headers.host, + device: headers.device, user: headers.user, - org_id: headers.membership.org_uuid, + membership: headers.membership, + ip: headers.ip, }) } else { err_handler!("You need to be a Member of the Organization to call this endpoint") @@ -891,6 +889,17 @@ impl<'r> FromRequest<'r> for OrgMemberHeaders { } } +impl From for Headers { + fn from(h: OrgMemberHeaders) -> Headers { + Headers { + host: h.host, + device: h.device, + user: h.user, + ip: h.ip, + } + } +} + // // Client IP address detection // diff --git a/src/config.rs b/src/config.rs index 3d001417..068735ff 100644 --- a/src/config.rs +++ b/src/config.rs @@ -579,7 +579,7 @@ make_config! { authenticator_disable_time_drift: bool, true, def, false; /// Customize the enabled feature flags on the clients |> This is a comma separated list of feature flags to enable. - experimental_client_feature_flags: String, false, def, "fido2-vault-credentials".to_string(); + experimental_client_feature_flags: String, false, def, String::new(); /// Require new device emails |> When a user logs in an email is required to be sent. /// If sending the email fails the login attempt will fail. @@ -833,21 +833,25 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { } } - // TODO: deal with deprecated flags so they can be removed from this list, cf. #4263 + // Server (v2025.5.0): https://github.com/bitwarden/server/blob/4a7db112a0952c6df8bacf36c317e9c4e58c3651/src/Core/Constants.cs#L102 + // Client (v2025.5.0): https://github.com/bitwarden/clients/blob/9df8a3cc50ed45f52513e62c23fcc8a4b745f078/libs/common/src/enums/feature-flag.enum.ts#L10 + // Android (v2025.4.0): https://github.com/bitwarden/android/blob/bee09de972c3870de0d54a0067996be473ec55c7/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt#L27 + // iOS (v2025.4.0): https://github.com/bitwarden/ios/blob/956e05db67344c912e3a1b8cb2609165d67da1c9/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7 + // + // NOTE: Move deprecated flags to the utils::parse_experimental_client_feature_flags() DEPRECATED_FLAGS const! const KNOWN_FLAGS: &[&str] = &[ - "autofill-overlay", - "autofill-v2", - "browser-fileless-import", - "extension-refresh", - "fido2-vault-credentials", + // Autofill Team "inline-menu-positioning-improvements", - "ssh-key-vault-item", + "inline-menu-totp", "ssh-agent", + // Key Management Team + "ssh-key-vault-item", + // Tools + "export-attachments", + // Mobile Team "anon-addy-self-host-alias", "simple-login-self-host-alias", "mutual-tls", - "export-attachments", - "inline-menu-totp", ]; let configured_flags = parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags); let invalid_flags: Vec<_> = configured_flags.keys().filter(|flag| !KNOWN_FLAGS.contains(&flag.as_str())).collect(); @@ -1373,6 +1377,7 @@ where reg!("email/email_footer_text"); reg!("email/admin_reset_password", ".html"); + reg!("email/change_email_existing", ".html"); reg!("email/change_email", ".html"); reg!("email/delete_account", ".html"); reg!("email/emergency_access_invite_accepted", ".html"); diff --git a/src/db/models/auth_request.rs b/src/db/models/auth_request.rs index 7f406581..2a14787e 100644 --- a/src/db/models/auth_request.rs +++ b/src/db/models/auth_request.rs @@ -16,7 +16,7 @@ db_object! { pub organization_uuid: Option, pub request_device_identifier: DeviceId, - pub device_type: i32, // https://github.com/bitwarden/server/blob/master/src/Core/Enums/DeviceType.cs + pub device_type: i32, // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs pub request_ip: String, pub response_device_id: Option, diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 089aaaf4..4a1b9789 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -318,7 +318,7 @@ impl Cipher { // supports the "cipherDetails" type, though it seems like the // Bitwarden clients will ignore extra fields. // - // Ref: https://github.com/bitwarden/server/blob/master/src/Core/Models/Api/Response/CipherResponseModel.cs + // Ref: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Vault/Models/Response/CipherResponseModel.cs#L14 let mut json_object = json!({ "object": "cipherDetails", "id": self.uuid, diff --git a/src/db/models/device.rs b/src/db/models/device.rs index b12bf70c..7cd23d5b 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -3,8 +3,12 @@ use derive_more::{Display, From}; use serde_json::Value; use super::{AuthRequest, UserId}; -use crate::{crypto, util::format_date, CONFIG}; -use macros::IdFromParam; +use crate::{ + crypto, + util::{format_date, get_uuid}, + CONFIG, +}; +use macros::{IdFromParam, UuidFromParam}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -19,8 +23,8 @@ db_object! { pub user_uuid: UserId, pub name: String, - pub atype: i32, // https://github.com/bitwarden/server/blob/dcc199bcce4aa2d5621f6fab80f1b49d8b143418/src/Core/Enums/DeviceType.cs - pub push_uuid: Option, + pub atype: i32, // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs + pub push_uuid: Option, pub push_token: Option, pub refresh_token: String, @@ -42,7 +46,7 @@ impl Device { name, atype, - push_uuid: None, + push_uuid: Some(PushId(get_uuid())), push_token: None, refresh_token: String::new(), twofactor_remember: None, @@ -54,7 +58,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 +77,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; @@ -84,6 +93,11 @@ impl Device { let time_now = Utc::now(); self.updated_at = time_now.naive_utc(); + // Generate a random push_uuid so if it doesn't already have one + if self.push_uuid.is_none() { + self.push_uuid = Some(PushId(get_uuid())); + } + // --- // Disabled these keys to be added to the JWT since they could cause the JWT to get too large // Also These key/value pairs are not used anywhere by either Vaultwarden or Bitwarden Clients @@ -121,6 +135,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()], }; @@ -132,10 +148,6 @@ impl Device { matches!(DeviceType::from_i32(self.atype), DeviceType::Android | DeviceType::Ios) } - pub fn is_registered(&self) -> bool { - self.push_uuid.is_some() - } - pub fn is_cli(&self) -> bool { matches!(DeviceType::from_i32(self.atype), DeviceType::WindowsCLI | DeviceType::MacOsCLI | DeviceType::LinuxCLI) } @@ -156,10 +168,12 @@ 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, + "encryptedPublicKey": null, + "encryptedUserKey": null, "object": "device", }) } @@ -395,3 +409,6 @@ impl DeviceType { Clone, Debug, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, IdFromParam, )] pub struct DeviceId(String); + +#[derive(Clone, Debug, DieselNewType, Display, From, FromForm, Serialize, Deserialize, UuidFromParam)] +pub struct PushId(pub String); diff --git a/src/db/models/emergency_access.rs b/src/db/models/emergency_access.rs index a82801f6..e3803b1a 100644 --- a/src/db/models/emergency_access.rs +++ b/src/db/models/emergency_access.rs @@ -78,6 +78,7 @@ impl EmergencyAccess { "grantorId": grantor_user.uuid, "email": grantor_user.email, "name": grantor_user.name, + "avatarColor": grantor_user.avatar_color, "object": "emergencyAccessGrantorDetails", }) } @@ -106,6 +107,7 @@ impl EmergencyAccess { "granteeId": grantee_user.uuid, "email": grantee_user.email, "name": grantee_user.name, + "avatarColor": grantee_user.avatar_color, "object": "emergencyAccessGranteeDetails", })) } diff --git a/src/db/models/event.rs b/src/db/models/event.rs index 985eca7e..76d12c02 100644 --- a/src/db/models/event.rs +++ b/src/db/models/event.rs @@ -8,9 +8,9 @@ use crate::{api::EmptyResult, db::DbConn, error::MapResult, CONFIG}; // https://bitwarden.com/help/event-logs/ db_object! { - // Upstream: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Services/Implementations/EventService.cs - // Upstream: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Api/Models/Public/Response/EventResponseModel.cs - // Upstream SQL: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Sql/dbo/Tables/Event.sql + // Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/EventService.cs + // Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Public/Models/Response/EventResponseModel.cs + // Upstream SQL: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Sql/dbo/Tables/Event.sql #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[diesel(table_name = event)] #[diesel(treat_none_as_null = true)] @@ -25,7 +25,7 @@ db_object! { pub group_uuid: Option, pub org_user_uuid: Option, pub act_user_uuid: Option, - // Upstream enum: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Enums/DeviceType.cs + // Upstream enum: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs pub device_type: Option, pub ip_address: Option, pub event_date: NaiveDateTime, @@ -36,7 +36,7 @@ db_object! { } } -// Upstream enum: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Enums/EventType.cs +// Upstream enum: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/EventType.cs #[derive(Debug, Copy, Clone)] pub enum EventType { // User @@ -72,7 +72,6 @@ pub enum EventType { CipherSoftDeleted = 1115, CipherRestored = 1116, CipherClientToggledCardNumberVisible = 1117, - CipherClientToggledTOTPSeedVisible = 1118, // Collection CollectionCreated = 1300, @@ -88,7 +87,7 @@ pub enum EventType { OrganizationUserInvited = 1500, OrganizationUserConfirmed = 1501, OrganizationUserUpdated = 1502, - OrganizationUserRemoved = 1503, + OrganizationUserRemoved = 1503, // Organization user data was deleted OrganizationUserUpdatedGroups = 1504, // OrganizationUserUnlinkedSso = 1505, // Not supported OrganizationUserResetPasswordEnroll = 1506, @@ -100,8 +99,8 @@ pub enum EventType { OrganizationUserRestored = 1512, OrganizationUserApprovedAuthRequest = 1513, OrganizationUserRejectedAuthRequest = 1514, - OrganizationUserDeleted = 1515, - OrganizationUserLeft = 1516, + OrganizationUserDeleted = 1515, // Both user and organization user data were deleted + OrganizationUserLeft = 1516, // User voluntarily left the organization // Organization OrganizationUpdated = 1600, @@ -188,7 +187,7 @@ impl Event { } /// Database methods -/// https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Services/Implementations/EventService.cs +/// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/EventService.cs impl Event { pub const PAGE_SIZE: i64 = 30; 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/mod.rs b/src/db/models/mod.rs index 90d17313..6b569a56 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -20,7 +20,7 @@ pub use self::attachment::{Attachment, AttachmentId}; pub use self::auth_request::{AuthRequest, AuthRequestId}; pub use self::cipher::{Cipher, CipherId, RepromptType}; pub use self::collection::{Collection, CollectionCipher, CollectionId, CollectionUser}; -pub use self::device::{Device, DeviceId, DeviceType}; +pub use self::device::{Device, DeviceId, DeviceType, PushId}; pub use self::emergency_access::{EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType}; pub use self::event::{Event, EventType}; pub use self::favorite::Favorite; diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index 7f139266..bc871427 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -21,7 +21,7 @@ db_object! { } } -// https://github.com/bitwarden/server/blob/abfdf6f5cb0f1f1504dbaaaa0e04ce9cb60faf19/src/Core/AdminConsole/Enums/PolicyType.cs +// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/PolicyType.cs #[derive(Copy, Clone, Eq, PartialEq, num_derive::FromPrimitive)] pub enum OrgPolicyType { TwoFactorAuthentication = 0, @@ -41,7 +41,7 @@ pub enum OrgPolicyType { RemoveUnlockWithPin = 14, } -// https://github.com/bitwarden/server/blob/5cbdee137921a19b1f722920f0fa3cd45af2ef0f/src/Core/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs +// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs#L5 #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct SendOptionsPolicyData { @@ -49,7 +49,7 @@ pub struct SendOptionsPolicyData { pub disable_hide_email: bool, } -// https://github.com/bitwarden/server/blob/5cbdee137921a19b1f722920f0fa3cd45af2ef0f/src/Core/Models/Data/Organizations/Policies/ResetPasswordDataModel.cs +// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/ResetPasswordDataModel.cs #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct ResetPasswordDataModel { @@ -83,14 +83,24 @@ impl OrgPolicy { pub fn to_json(&self) -> Value { let data_json: Value = serde_json::from_str(&self.data).unwrap_or(Value::Null); - json!({ + let mut policy = json!({ "id": self.uuid, "organizationId": self.org_uuid, "type": self.atype, "data": data_json, "enabled": self.enabled, "object": "policy", - }) + }); + + // Upstream adds this key/value + // Allow enabling Single Org policy when the organization has claimed domains. + // See: (https://github.com/bitwarden/server/pull/5565) + // We return the same to prevent possible issues + if self.atype == 8i32 { + policy["canToggleState"] = json!(true); + } + + policy } } diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index d62117c3..f51a9725 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -56,7 +56,7 @@ db_object! { } } -// https://github.com/bitwarden/server/blob/b86a04cef9f1e1b82cf18e49fc94e017c641130c/src/Core/Enums/OrganizationUserStatusType.cs +// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/OrganizationUserStatusType.cs #[derive(PartialEq)] pub enum MembershipStatus { Revoked = -1, @@ -177,7 +177,7 @@ impl Organization { public_key, } } - // https://github.com/bitwarden/server/blob/13d1e74d6960cf0d042620b72d85bf583a4236f7/src/Api/Models/Response/Organizations/OrganizationResponseModel.cs + // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs pub fn to_json(&self) -> Value { json!({ "id": self.uuid, @@ -203,7 +203,6 @@ impl Organization { "useResetPassword": CONFIG.mail_enabled(), "allowAdminAccessToAllCollectionItems": true, "limitCollectionCreation": true, - "limitCollectionCreationDeletion": true, "limitCollectionDeletion": true, "businessName": self.name, @@ -424,7 +423,7 @@ impl Membership { "manageScim": false // Not supported (Not AGPLv3 Licensed) }); - // https://github.com/bitwarden/server/blob/13d1e74d6960cf0d042620b72d85bf583a4236f7/src/Api/Models/Response/ProfileOrganizationResponseModel.cs + // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs json!({ "id": self.org_uuid, "identifier": null, // Not supported @@ -451,6 +450,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 +459,6 @@ impl Membership { "providerType": null, "familySponsorshipFriendlyName": null, "familySponsorshipAvailable": false, - "planProductType": 3, "productTierType": 3, // Enterprise tier "keyConnectorEnabled": false, "keyConnectorUrl": null, @@ -467,10 +467,11 @@ impl Membership { "familySponsorshipToDelete": null, "accessSecretsManager": false, "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 +617,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 +866,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, diff --git a/src/error.rs b/src/error.rs index 754bece3..a7a8ab07 100644 --- a/src/error.rs +++ b/src/error.rs @@ -59,6 +59,8 @@ use yubico::yubicoerror::YubicoError as YubiErr; #[derive(Serialize)] pub struct Empty {} +pub struct Compact {} + // Error struct // Contains a String error message, meant for the user and an enum variant, with an error of different types. // @@ -69,6 +71,7 @@ make_error! { Empty(Empty): _no_source, _serialize, // Used to represent err! calls Simple(String): _no_source, _api_error, + Compact(Compact): _no_source, _api_error_small, // Used in our custom http client to handle non-global IPs and blocked domains CustomHttpClient(CustomHttpClientError): _has_source, _api_error, @@ -132,6 +135,12 @@ impl Error { self } + #[must_use] + pub fn with_kind(mut self, kind: ErrorKind) -> Self { + self.error = kind; + self + } + #[must_use] pub const fn with_code(mut self, code: u16) -> Self { self.error_code = code; @@ -200,6 +209,18 @@ fn _api_error(_: &impl std::any::Any, msg: &str) -> String { _serialize(&json, "") } +fn _api_error_small(_: &impl std::any::Any, msg: &str) -> String { + let json = json!({ + "message": msg, + "validationErrors": null, + "exceptionMessage": null, + "exceptionStackTrace": null, + "innerExceptionMessage": null, + "object": "error" + }); + _serialize(&json, "") +} + // // Rocket responder impl // @@ -212,8 +233,7 @@ use rocket::response::{self, Responder, Response}; impl Responder<'_, 'static> for Error { fn respond_to(self, _: &Request<'_>) -> response::Result<'static> { match self.error { - ErrorKind::Empty(_) => {} // Don't print the error in this situation - ErrorKind::Simple(_) => {} // Don't print the error in this situation + ErrorKind::Empty(_) | ErrorKind::Simple(_) | ErrorKind::Compact(_) => {} // Don't print the error in this situation _ => error!(target: "error", "{self:#?}"), }; @@ -228,6 +248,10 @@ impl Responder<'_, 'static> for Error { // #[macro_export] macro_rules! err { + ($kind:ident, $msg:expr) => {{ + error!("{}", $msg); + return Err($crate::error::Error::new($msg, $msg).with_kind($crate::error::ErrorKind::$kind($crate::error::$kind {}))); + }}; ($msg:expr) => {{ error!("{}", $msg); return Err($crate::error::Error::new($msg, $msg)); diff --git a/src/mail.rs b/src/mail.rs index 8100815e..b1f37886 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -570,6 +570,20 @@ pub async fn send_change_email(address: &str, token: &str) -> EmptyResult { send_email(address, &subject, body_html, body_text).await } +pub async fn send_change_email_existing(address: &str, acting_address: &str) -> EmptyResult { + let (subject, body_html, body_text) = get_text( + "email/change_email_existing", + json!({ + "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), + "existing_address": address, + "acting_address": acting_address, + }), + )?; + + send_email(address, &subject, body_html, body_text).await +} + pub async fn send_test(address: &str) -> EmptyResult { let (subject, body_html, body_text) = get_text( "email/smtp_test", diff --git a/src/static/templates/email/change_email_existing.hbs b/src/static/templates/email/change_email_existing.hbs new file mode 100644 index 00000000..ccbcb6ad --- /dev/null +++ b/src/static/templates/email/change_email_existing.hbs @@ -0,0 +1,6 @@ +Your Email Change + +A user ({{ acting_address }}) recently tried to change their account to use this email address ({{ existing_address }}). An account already exists with this email ({{ existing_address }}). + +If you did not try to change an email address, contact your administrator. +{{> email/email_footer_text }} diff --git a/src/static/templates/email/change_email_existing.html.hbs b/src/static/templates/email/change_email_existing.html.hbs new file mode 100644 index 00000000..aa15f612 --- /dev/null +++ b/src/static/templates/email/change_email_existing.html.hbs @@ -0,0 +1,16 @@ +Your Email Change + +{{> email/email_header }} + + + + + + + +
+ A user ({{ acting_address }}) recently tried to change their account to use this email address ({{ existing_address }}). An account already exists with this email ({{ existing_address }}). +
+ If you did not try to change an email address, contact your administrator. +
+{{> email/email_footer }} diff --git a/src/static/templates/email/send_2fa_removed_from_org.hbs b/src/static/templates/email/send_2fa_removed_from_org.hbs index 79956bbb..ef98569e 100644 --- a/src/static/templates/email/send_2fa_removed_from_org.hbs +++ b/src/static/templates/email/send_2fa_removed_from_org.hbs @@ -1,7 +1,7 @@ -Removed from {{{org_name}}} +Your access to {{{org_name}}} has been revoked. -You have been removed from organization *{{org_name}}* because your account does not have Two-step Login enabled. +Your user account has been removed from the *{{org_name}}* organization because you do not have two-step login configured. +Before you can re-join this organization you need to set up two-step login on your user account. - -You can enable Two-step Login in your account settings. +You can enable two-step login in your account settings. {{> email/email_footer_text }} diff --git a/src/static/templates/email/send_2fa_removed_from_org.html.hbs b/src/static/templates/email/send_2fa_removed_from_org.html.hbs index 6588a320..ed397dd3 100644 --- a/src/static/templates/email/send_2fa_removed_from_org.html.hbs +++ b/src/static/templates/email/send_2fa_removed_from_org.html.hbs @@ -1,15 +1,16 @@ -Removed from {{{org_name}}} +Your access to {{{org_name}}} has been revoked. {{> email/email_header }}
- You have been removed from organization {{org_name}} because your account does not have Two-step Login enabled. + Your user account has been removed from the {{org_name}} organization because you do not have two-step login configured.
+ Before you can re-join this organization you need to set up two-step login on your user account.
- You can enable Two-step Login in your account settings. + You can enable two-step login in your account settings.
diff --git a/src/util.rs b/src/util.rs index c72f23ce..2d6dc3a5 100644 --- a/src/util.rs +++ b/src/util.rs @@ -752,9 +752,20 @@ pub fn convert_json_key_lcase_first(src_json: Value) -> Value { /// Parses the experimental client feature flags string into a HashMap. pub fn parse_experimental_client_feature_flags(experimental_client_feature_flags: &str) -> HashMap { - let feature_states = experimental_client_feature_flags.split(',').map(|f| (f.trim().to_owned(), true)).collect(); - - feature_states + // These flags could still be configured, but are deprecated and not used anymore + // To prevent old installations from starting filter these out and not error out + const DEPRECATED_FLAGS: &[&str] = + &["autofill-overlay", "autofill-v2", "browser-fileless-import", "extension-refresh", "fido2-vault-credentials"]; + experimental_client_feature_flags + .split(',') + .filter_map(|f| { + let flag = f.trim(); + if !flag.is_empty() && !DEPRECATED_FLAGS.contains(&flag) { + return Some((flag.to_owned(), true)); + } + None + }) + .collect() } /// TODO: This is extracted from IpAddr::is_global, which is unstable: