From e8d8a816dd79923c309d64e42d6476faeb76df7c Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 11 Dec 2024 03:53:00 -0800 Subject: [PATCH 1/8] [PM-14988] Use peercred / GetNamedPipeClientProcessId to gather info about process connecting to ssh agent (#12065) * Fix double prompt when unlocking by ssh request * Add peercred for unix * Enable apple-app-store feature * Add generic parameter * Update * Add procinfo for windows * Show connecting app in ui * Use struct instead of tuple * Use atomics instead of mutex * Fix windows build * Use is_running function * Cleanup named pipe listener * Cleanups * Cargo fmt * Replace "" with none * Rebuild index.d.ts * Fix is running check --- apps/desktop/desktop_native/Cargo.lock | 71 +++++++++++++++- apps/desktop/desktop_native/core/Cargo.toml | 4 +- .../core/src/biometric/windows.rs | 19 +++-- .../desktop_native/core/src/password/macos.rs | 12 ++- .../desktop_native/core/src/password/unix.rs | 48 ++++++----- .../core/src/password/windows.rs | 12 ++- .../desktop_native/core/src/ssh_agent/mod.rs | 83 +++++++++++++------ .../ssh_agent/named_pipe_listener_stream.rs | 49 ++++++++--- .../peercred_unix_listener_stream.rs | 72 ++++++++++++++++ .../core/src/ssh_agent/peerinfo/gather.rs | 23 +++++ .../core/src/ssh_agent/peerinfo/mod.rs | 2 + .../core/src/ssh_agent/peerinfo/models.rs | 32 +++++++ .../desktop_native/core/src/ssh_agent/unix.rs | 30 ++++--- .../core/src/ssh_agent/windows.rs | 23 +++-- apps/desktop/desktop_native/napi/index.d.ts | 2 +- apps/desktop/desktop_native/napi/src/lib.rs | 24 +++--- .../napi/src/registry/windows.rs | 2 +- .../platform/main/main-ssh-agent.service.ts | 3 +- .../platform/services/ssh-agent.service.ts | 6 +- 19 files changed, 411 insertions(+), 106 deletions(-) create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/peercred_unix_listener_stream.rs create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/gather.rs create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/mod.rs create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/models.rs diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 1cf8b24c265..09d3d15e897 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -338,7 +338,7 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bitwarden-russh" version = "0.1.0" -source = "git+https://github.com/bitwarden/bitwarden-russh.git?rev=b4e7f2fedbe3df8c35545feb000176d3e7b2bc32#b4e7f2fedbe3df8c35545feb000176d3e7b2bc32" +source = "git+https://github.com/bitwarden/bitwarden-russh.git?rev=23b50e3bbe6d56ef19ab0e98e8bb1462cb6d77ae#23b50e3bbe6d56ef19ab0e98e8bb1462cb6d77ae" dependencies = [ "anyhow", "byteorder", @@ -584,6 +584,25 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -773,6 +792,7 @@ dependencies = [ "sha2", "ssh-encoding", "ssh-key", + "sysinfo", "thiserror", "tokio", "tokio-stream", @@ -903,6 +923,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "embed_plist" version = "1.2.2" @@ -1490,6 +1516,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + [[package]] name = "num" version = "0.4.3" @@ -2051,6 +2086,26 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "recvmsg" version = "1.0.0" @@ -2453,6 +2508,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sysinfo" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ae3f4f7d64646c46c4cae4e3f01d1c5d255c7406fdd7c7f999a94e488791" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] + [[package]] name = "tempfile" version = "3.14.0" diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index adfdd818a17..597a082b231 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -49,7 +49,7 @@ ssh-key = { version = "=0.6.7", default-features = false, features = [ "rsa", "getrandom", ] } -bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "b4e7f2fedbe3df8c35545feb000176d3e7b2bc32" } +bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "23b50e3bbe6d56ef19ab0e98e8bb1462cb6d77ae" } tokio = { version = "=1.41.1", features = ["io-util", "sync", "macros", "net"] } tokio-stream = { version = "=0.1.15", features = ["net"] } tokio-util = { version = "=0.7.12", features = ["codec"] } @@ -59,6 +59,7 @@ rand_chacha = "=0.3.1" pkcs8 = { version = "=0.10.2", features = ["alloc", "encryption", "pem"] } rsa = "=0.9.6" ed25519 = { version = "=2.2.3", features = ["pkcs8"] } +sysinfo = { version = "0.32.0", features = ["windows"] } [target.'cfg(windows)'.dependencies] widestring = { version = "=1.1.0", optional = true } @@ -72,6 +73,7 @@ windows = { version = "=0.58.0", features = [ "Win32_System_WinRT", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_WindowsAndMessaging", + "Win32_System_Pipes", ], optional = true } [target.'cfg(windows)'.dev-dependencies] diff --git a/apps/desktop/desktop_native/core/src/biometric/windows.rs b/apps/desktop/desktop_native/core/src/biometric/windows.rs index fcc5b95cc4a..d17ea752878 100644 --- a/apps/desktop/desktop_native/core/src/biometric/windows.rs +++ b/apps/desktop/desktop_native/core/src/biometric/windows.rs @@ -310,12 +310,16 @@ mod tests { os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(), client_key_part_b64: Some("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned()), }; - crate::password::set_password(test, test, secret).await.unwrap(); + crate::password::set_password(test, test, secret) + .await + .unwrap(); let result = ::get_biometric_secret(test, test, Some(key_material)) .await .unwrap(); - crate::password::delete_password("test", "test").await.unwrap(); + crate::password::delete_password("test", "test") + .await + .unwrap(); assert_eq!(result, secret); } @@ -328,19 +332,24 @@ mod tests { os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(), client_key_part_b64: Some("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned()), }; - crate::password::set_password(test, test, &secret.to_string()).await.unwrap(); + crate::password::set_password(test, test, &secret.to_string()) + .await + .unwrap(); let result = ::get_biometric_secret(test, test, Some(key_material)) .await .unwrap(); - crate::password::delete_password("test", "test").await.unwrap(); + crate::password::delete_password("test", "test") + .await + .unwrap(); assert_eq!(result, "secret"); } #[tokio::test] async fn set_biometric_secret_requires_key() { - let result = ::set_biometric_secret("", "", "", None, "").await; + let result = + ::set_biometric_secret("", "", "", None, "").await; assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), diff --git a/apps/desktop/desktop_native/core/src/password/macos.rs b/apps/desktop/desktop_native/core/src/password/macos.rs index c911a0d2430..b69854905d9 100644 --- a/apps/desktop/desktop_native/core/src/password/macos.rs +++ b/apps/desktop/desktop_native/core/src/password/macos.rs @@ -28,12 +28,18 @@ mod tests { #[tokio::test] async fn test() { - set_password("BitwardenTest", "BitwardenTest", "Random").await.unwrap(); + set_password("BitwardenTest", "BitwardenTest", "Random") + .await + .unwrap(); assert_eq!( "Random", - get_password("BitwardenTest", "BitwardenTest").await.unwrap() + get_password("BitwardenTest", "BitwardenTest") + .await + .unwrap() ); - delete_password("BitwardenTest", "BitwardenTest").await.unwrap(); + delete_password("BitwardenTest", "BitwardenTest") + .await + .unwrap(); // Ensure password is deleted match get_password("BitwardenTest", "BitwardenTest").await { diff --git a/apps/desktop/desktop_native/core/src/password/unix.rs b/apps/desktop/desktop_native/core/src/password/unix.rs index 20a79625efb..f73b41de8c1 100644 --- a/apps/desktop/desktop_native/core/src/password/unix.rs +++ b/apps/desktop/desktop_native/core/src/password/unix.rs @@ -5,9 +5,7 @@ use std::collections::HashMap; pub async fn get_password(service: &str, account: &str) -> Result { match get_password_new(service, account).await { Ok(res) => Ok(res), - Err(_) => { - get_password_legacy(service, account).await - } + Err(_) => get_password_legacy(service, account).await, } } @@ -20,8 +18,8 @@ async fn get_password_new(service: &str, account: &str) -> Result { Some(res) => { let secret = res.secret().await?; Ok(String::from_utf8(secret.to_vec())?) - }, - None => Err(anyhow!("no result")) + } + None => Err(anyhow!("no result")), } } @@ -37,20 +35,30 @@ async fn get_password_legacy(service: &str, account: &str) -> Result { match res { Some(res) => { let secret = res.secret().await?; - println!("deleting legacy secret service entry {} {}", service, account); + println!( + "deleting legacy secret service entry {} {}", + service, account + ); keyring.delete(&attributes).await?; let secret_string = String::from_utf8(secret.to_vec())?; set_password(service, account, &secret_string).await?; Ok(secret_string) - }, - None => Err(anyhow!("no result")) + } + None => Err(anyhow!("no result")), } } pub async fn set_password(service: &str, account: &str, password: &str) -> Result<()> { let keyring = oo7::Keyring::new().await?; let attributes = HashMap::from([("service", service), ("account", account)]); - keyring.create_item("org.freedesktop.Secret.Generic", &attributes, password, true).await?; + keyring + .create_item( + "org.freedesktop.Secret.Generic", + &attributes, + password, + true, + ) + .await?; Ok(()) } @@ -74,22 +82,25 @@ mod tests { #[tokio::test] async fn test() { - set_password("BitwardenTest", "BitwardenTest", "Random").await.unwrap(); + set_password("BitwardenTest", "BitwardenTest", "Random") + .await + .unwrap(); assert_eq!( "Random", - get_password("BitwardenTest", "BitwardenTest").await.unwrap() + get_password("BitwardenTest", "BitwardenTest") + .await + .unwrap() ); - delete_password("BitwardenTest", "BitwardenTest").await.unwrap(); + delete_password("BitwardenTest", "BitwardenTest") + .await + .unwrap(); // Ensure password is deleted match get_password("BitwardenTest", "BitwardenTest").await { Ok(_) => { panic!("Got a result") } - Err(e) => assert_eq!( - "no result", - e.to_string() - ), + Err(e) => assert_eq!("no result", e.to_string()), } } @@ -97,10 +108,7 @@ mod tests { async fn test_error_no_password() { match get_password("Unknown", "Unknown").await { Ok(_) => panic!("Got a result"), - Err(e) => assert_eq!( - "no result", - e.to_string() - ), + Err(e) => assert_eq!("no result", e.to_string()), } } } diff --git a/apps/desktop/desktop_native/core/src/password/windows.rs b/apps/desktop/desktop_native/core/src/password/windows.rs index 873e717ac8b..2a66640286f 100644 --- a/apps/desktop/desktop_native/core/src/password/windows.rs +++ b/apps/desktop/desktop_native/core/src/password/windows.rs @@ -112,12 +112,18 @@ mod tests { #[tokio::test] async fn test() { - set_password("BitwardenTest", "BitwardenTest", "Random").await.unwrap(); + set_password("BitwardenTest", "BitwardenTest", "Random") + .await + .unwrap(); assert_eq!( "Random", - get_password("BitwardenTest", "BitwardenTest").await.unwrap() + get_password("BitwardenTest", "BitwardenTest") + .await + .unwrap() ); - delete_password("BitwardenTest", "BitwardenTest").await.unwrap(); + delete_password("BitwardenTest", "BitwardenTest") + .await + .unwrap(); // Ensure password is deleted match get_password("BitwardenTest", "BitwardenTest").await { diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs index 9d04ea87ccb..82b90c7bff9 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs @@ -1,4 +1,7 @@ -use std::sync::Arc; +use std::sync::{ + atomic::{AtomicBool, AtomicU32}, + Arc, +}; use tokio::sync::Mutex; use tokio_util::sync::CancellationToken; @@ -10,34 +13,52 @@ use bitwarden_russh::ssh_agent::{self, Key}; #[cfg_attr(target_os = "linux", path = "unix.rs")] mod platform_ssh_agent; +#[cfg(any(target_os = "linux", target_os = "macos"))] +mod peercred_unix_listener_stream; + pub mod generator; pub mod importer; - +pub mod peerinfo; #[derive(Clone)] pub struct BitwardenDesktopAgent { keystore: ssh_agent::KeyStore, cancellation_token: CancellationToken, - show_ui_request_tx: tokio::sync::mpsc::Sender<(u32, (String, bool))>, + show_ui_request_tx: tokio::sync::mpsc::Sender, get_ui_response_rx: Arc>>, - request_id: Arc>, + request_id: Arc, /// before first unlock, or after account switching, listing keys should require an unlock to get a list of public keys - needs_unlock: Arc>, - is_running: Arc>, + needs_unlock: Arc, + is_running: Arc, } -impl ssh_agent::Agent for BitwardenDesktopAgent { - async fn confirm(&self, ssh_key: Key) -> bool { - if !*self.is_running.lock().await { +pub struct SshAgentUIRequest { + pub request_id: u32, + pub cipher_id: Option, + pub process_name: String, + pub is_list: bool, +} + +impl ssh_agent::Agent for BitwardenDesktopAgent { + async fn confirm(&self, ssh_key: Key, info: &peerinfo::models::PeerInfo) -> bool { + if !self.is_running() { println!("[BitwardenDesktopAgent] Agent is not running, but tried to call confirm"); return false; } let request_id = self.get_request_id().await; + println!( + "[SSH Agent] Confirming request from application: {}", + info.process_name() + ); let mut rx_channel = self.get_ui_response_rx.lock().await.resubscribe(); - let message = (request_id, (ssh_key.cipher_uuid.clone(), false)); self.show_ui_request_tx - .send(message) + .send(SshAgentUIRequest { + request_id, + cipher_id: Some(ssh_key.cipher_uuid.clone()), + process_name: info.process_name().to_string(), + is_list: false, + }) .await .expect("Should send request to ui"); while let Ok((id, response)) = rx_channel.recv().await { @@ -48,15 +69,20 @@ impl ssh_agent::Agent for BitwardenDesktopAgent { false } - async fn can_list(&self) -> bool { - if !*self.needs_unlock.lock().await{ + async fn can_list(&self, info: &peerinfo::models::PeerInfo) -> bool { + if !self.needs_unlock.load(std::sync::atomic::Ordering::Relaxed) { return true; } let request_id = self.get_request_id().await; let mut rx_channel = self.get_ui_response_rx.lock().await.resubscribe(); - let message = (request_id, ("".to_string(), true)); + let message = SshAgentUIRequest { + request_id, + cipher_id: None, + process_name: info.process_name().to_string(), + is_list: true, + }; self.show_ui_request_tx .send(message) .await @@ -72,13 +98,13 @@ impl ssh_agent::Agent for BitwardenDesktopAgent { impl BitwardenDesktopAgent { pub fn stop(&self) { - if !*self.is_running.blocking_lock() { + if !self.is_running() { println!("[BitwardenDesktopAgent] Tried to stop agent while it is not running"); return; } - *self.is_running.blocking_lock() = false; - self.cancellation_token.cancel(); + self.is_running + .store(false, std::sync::atomic::Ordering::Relaxed); self.keystore .0 .write() @@ -90,7 +116,7 @@ impl BitwardenDesktopAgent { &mut self, new_keys: Vec<(String, String, String)>, ) -> Result<(), anyhow::Error> { - if !*self.is_running.blocking_lock() { + if !self.is_running() { return Err(anyhow::anyhow!( "[BitwardenDesktopAgent] Tried to set keys while agent is not running" )); @@ -99,7 +125,8 @@ impl BitwardenDesktopAgent { let keystore = &mut self.keystore; keystore.0.write().expect("RwLock is not poisoned").clear(); - *self.needs_unlock.blocking_lock() = false; + self.needs_unlock + .store(true, std::sync::atomic::Ordering::Relaxed); for (key, name, cipher_id) in new_keys.iter() { match parse_key_safe(&key) { @@ -127,7 +154,7 @@ impl BitwardenDesktopAgent { } pub fn lock(&mut self) -> Result<(), anyhow::Error> { - if !*self.is_running.blocking_lock() { + if !self.is_running() { return Err(anyhow::anyhow!( "[BitwardenDesktopAgent] Tried to lock agent, but it is not running" )); @@ -148,24 +175,26 @@ impl BitwardenDesktopAgent { pub fn clear_keys(&mut self) -> Result<(), anyhow::Error> { let keystore = &mut self.keystore; keystore.0.write().expect("RwLock is not poisoned").clear(); - *self.needs_unlock.blocking_lock() = true; + self.needs_unlock + .store(true, std::sync::atomic::Ordering::Relaxed); Ok(()) } async fn get_request_id(&self) -> u32 { - if !*self.is_running.lock().await { + if !self.is_running() { println!("[BitwardenDesktopAgent] Agent is not running, but tried to get request id"); return 0; } - let mut request_id = self.request_id.lock().await; - *request_id += 1; - *request_id + let request_id = self + .request_id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + request_id } - pub fn is_running(self) -> bool { - return self.is_running.blocking_lock().clone(); + pub fn is_running(&self) -> bool { + self.is_running.load(std::sync::atomic::Ordering::Relaxed) } } diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs b/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs index 49c3aa80612..1358abe32e0 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs @@ -1,23 +1,32 @@ -use std::{ - io, pin::Pin, sync::Arc, task::{Context, Poll} -}; - use futures::Stream; +use std::os::windows::prelude::AsRawHandle as _; +use std::{ + io, + pin::Pin, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + task::{Context, Poll}, +}; use tokio::{ net::windows::named_pipe::{NamedPipeServer, ServerOptions}, select, }; use tokio_util::sync::CancellationToken; +use windows::Win32::{Foundation::HANDLE, System::Pipes::GetNamedPipeClientProcessId}; + +use crate::ssh_agent::peerinfo::{self, models::PeerInfo}; const PIPE_NAME: &str = r"\\.\pipe\openssh-ssh-agent"; #[pin_project::pin_project] pub struct NamedPipeServerStream { - rx: tokio::sync::mpsc::Receiver, + rx: tokio::sync::mpsc::Receiver<(NamedPipeServer, PeerInfo)>, } impl NamedPipeServerStream { - pub fn new(cancellation_token: CancellationToken, is_running: Arc>) -> Self { + pub fn new(cancellation_token: CancellationToken, is_running: Arc) -> Self { let (tx, rx) = tokio::sync::mpsc::channel(16); tokio::spawn(async move { println!( @@ -30,7 +39,7 @@ impl NamedPipeServerStream { println!("[SSH Agent Native Module] Encountered an error creating the first pipe. The system's openssh service must likely be disabled"); println!("[SSH Agent Natvie Module] error: {}", err); cancellation_token.cancel(); - *is_running.lock().await = false; + is_running.store(false, Ordering::Relaxed); return; } }; @@ -43,14 +52,32 @@ impl NamedPipeServerStream { } _ = listener.connect() => { println!("[SSH Agent Native Module] Incoming connection"); - tx.send(listener).await.unwrap(); + let handle = HANDLE(listener.as_raw_handle()); + let mut pid = 0; + unsafe { + if let Err(e) = GetNamedPipeClientProcessId(handle, &mut pid) { + println!("Error getting named pipe client process id {}", e); + continue + } + }; + + let peer_info = peerinfo::gather::get_peer_info(pid as u32); + let peer_info = match peer_info { + Err(err) => { + println!("Failed getting process info for pid {} {}", pid, err); + continue + }, + Ok(info) => info, + }; + + tx.send((listener, peer_info)).await.unwrap(); listener = match ServerOptions::new().create(PIPE_NAME) { Ok(pipe) => pipe, Err(err) => { println!("[SSH Agent Native Module] Encountered an error creating a new pipe {}", err); cancellation_token.cancel(); - *is_running.lock().await = false; + is_running.store(false, Ordering::Relaxed); return; } }; @@ -63,12 +90,12 @@ impl NamedPipeServerStream { } impl Stream for NamedPipeServerStream { - type Item = io::Result; + type Item = io::Result<(NamedPipeServer, PeerInfo)>; fn poll_next( self: Pin<&mut Self>, cx: &mut Context<'_>, - ) -> Poll>> { + ) -> Poll>> { let this = self.project(); this.rx.poll_recv(cx).map(|v| v.map(Ok)) diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/peercred_unix_listener_stream.rs b/apps/desktop/desktop_native/core/src/ssh_agent/peercred_unix_listener_stream.rs new file mode 100644 index 00000000000..f0114fc08da --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/peercred_unix_listener_stream.rs @@ -0,0 +1,72 @@ +use futures::Stream; +use std::io; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::net::{UnixListener, UnixStream}; + +use super::peerinfo; +use super::peerinfo::models::PeerInfo; + +#[derive(Debug)] +pub struct PeercredUnixListenerStream { + inner: UnixListener, +} + +impl PeercredUnixListenerStream { + pub fn new(listener: UnixListener) -> Self { + Self { inner: listener } + } +} + +impl Stream for PeercredUnixListenerStream { + type Item = io::Result<(UnixStream, PeerInfo)>; + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + match self.inner.poll_accept(cx) { + Poll::Ready(Ok((stream, _))) => { + let pid = match stream.peer_cred() { + Ok(peer) => match peer.pid() { + Some(pid) => pid, + None => { + return Poll::Ready(Some(Err(io::Error::new( + io::ErrorKind::Other, + "Failed to get peer PID", + )))); + } + }, + Err(err) => { + return Poll::Ready(Some(Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to get peer credentials: {}", err), + )))); + } + }; + let peer_info = peerinfo::gather::get_peer_info(pid as u32); + match peer_info { + Ok(info) => Poll::Ready(Some(Ok((stream, info)))), + Err(err) => Poll::Ready(Some(Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to get peer info: {}", err), + )))), + } + } + Poll::Ready(Err(err)) => Poll::Ready(Some(Err(err))), + Poll::Pending => Poll::Pending, + } + } +} + +impl AsRef for PeercredUnixListenerStream { + fn as_ref(&self) -> &UnixListener { + &self.inner + } +} + +impl AsMut for PeercredUnixListenerStream { + fn as_mut(&mut self) -> &mut UnixListener { + &mut self.inner + } +} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/gather.rs b/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/gather.rs new file mode 100644 index 00000000000..699203d613d --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/gather.rs @@ -0,0 +1,23 @@ +use sysinfo::{Pid, System}; + +use super::models::PeerInfo; + +pub fn get_peer_info(peer_pid: u32) -> Result { + let s = System::new_all(); + if let Some(process) = s.process(Pid::from_u32(peer_pid)) { + let peer_process_name = match process.name().to_str() { + Some(name) => name.to_string(), + None => { + return Err("Failed to get process name".to_string()); + } + }; + + return Ok(PeerInfo::new( + peer_pid, + process.pid().as_u32(), + peer_process_name, + )); + } + + Err("Failed to get process".to_string()) +} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/mod.rs b/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/mod.rs new file mode 100644 index 00000000000..fb12aa66e09 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/mod.rs @@ -0,0 +1,2 @@ +pub mod gather; +pub mod models; diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/models.rs b/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/models.rs new file mode 100644 index 00000000000..823d912883e --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/models.rs @@ -0,0 +1,32 @@ +/** +* Peerinfo represents the information of a peer process connecting over a socket. +* This can be later extended to include more information (icon, app name) for the corresponding application. +*/ +#[derive(Debug)] +pub struct PeerInfo { + uid: u32, + pid: u32, + process_name: String, +} + +impl PeerInfo { + pub fn new(uid: u32, pid: u32, process_name: String) -> Self { + Self { + uid, + pid, + process_name, + } + } + + pub fn uid(&self) -> u32 { + self.uid + } + + pub fn pid(&self) -> u32 { + self.pid + } + + pub fn process_name(&self) -> &str { + &self.process_name + } +} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs b/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs index ed2fe9ffab1..a74c1205b57 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs @@ -2,7 +2,10 @@ use std::{ collections::HashMap, fs, os::unix::fs::PermissionsExt, - sync::{Arc, RwLock}, + sync::{ + atomic::{AtomicBool, AtomicU32}, + Arc, RwLock, + }, }; use bitwarden_russh::ssh_agent; @@ -10,11 +13,13 @@ use homedir::my_home; use tokio::{net::UnixListener, sync::Mutex}; use tokio_util::sync::CancellationToken; -use super::BitwardenDesktopAgent; +use crate::ssh_agent::peercred_unix_listener_stream::PeercredUnixListenerStream; + +use super::{BitwardenDesktopAgent, SshAgentUIRequest}; impl BitwardenDesktopAgent { pub async fn start_server( - auth_request_tx: tokio::sync::mpsc::Sender<(u32, (String, bool))>, + auth_request_tx: tokio::sync::mpsc::Sender, auth_response_rx: Arc>>, ) -> Result { let agent = BitwardenDesktopAgent { @@ -22,9 +27,9 @@ impl BitwardenDesktopAgent { cancellation_token: CancellationToken::new(), show_ui_request_tx: auth_request_tx, get_ui_response_rx: auth_response_rx, - request_id: Arc::new(tokio::sync::Mutex::new(0)), - needs_unlock: Arc::new(tokio::sync::Mutex::new(true)), - is_running: Arc::new(tokio::sync::Mutex::new(false)), + request_id: Arc::new(AtomicU32::new(0)), + needs_unlock: Arc::new(AtomicBool::new(false)), + is_running: Arc::new(AtomicBool::new(false)), }; let cloned_agent_state = agent.clone(); tokio::spawn(async move { @@ -75,18 +80,23 @@ impl BitwardenDesktopAgent { return; } - let wrapper = tokio_stream::wrappers::UnixListenerStream::new(listener); + let stream = PeercredUnixListenerStream::new(listener); + let cloned_keystore = cloned_agent_state.keystore.clone(); let cloned_cancellation_token = cloned_agent_state.cancellation_token.clone(); - *cloned_agent_state.is_running.lock().await = true; + cloned_agent_state + .is_running + .store(true, std::sync::atomic::Ordering::Relaxed); let _ = ssh_agent::serve( - wrapper, + stream, cloned_agent_state.clone(), cloned_keystore, cloned_cancellation_token, ) .await; - *cloned_agent_state.is_running.lock().await = false; + cloned_agent_state + .is_running + .store(false, std::sync::atomic::Ordering::Relaxed); println!("[SSH Agent Native Module] SSH Agent server exited"); } Err(e) => { diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs b/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs index 6a99b7cfb00..bc63ef552b7 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs @@ -3,16 +3,19 @@ pub mod named_pipe_listener_stream; use std::{ collections::HashMap, - sync::{Arc, RwLock}, + sync::{ + atomic::{AtomicBool, AtomicU32}, + Arc, RwLock, + }, }; use tokio::sync::Mutex; use tokio_util::sync::CancellationToken; -use super::BitwardenDesktopAgent; +use super::{BitwardenDesktopAgent, SshAgentUIRequest}; impl BitwardenDesktopAgent { pub async fn start_server( - auth_request_tx: tokio::sync::mpsc::Sender<(u32, (String, bool))>, + auth_request_tx: tokio::sync::mpsc::Sender, auth_response_rx: Arc>>, ) -> Result { let agent_state = BitwardenDesktopAgent { @@ -20,9 +23,9 @@ impl BitwardenDesktopAgent { show_ui_request_tx: auth_request_tx, get_ui_response_rx: auth_response_rx, cancellation_token: CancellationToken::new(), - request_id: Arc::new(tokio::sync::Mutex::new(0)), - needs_unlock: Arc::new(tokio::sync::Mutex::new(true)), - is_running: Arc::new(tokio::sync::Mutex::new(true)), + request_id: Arc::new(AtomicU32::new(0)), + needs_unlock: Arc::new(AtomicBool::new(true)), + is_running: Arc::new(AtomicBool::new(true)), }; let stream = named_pipe_listener_stream::NamedPipeServerStream::new( agent_state.cancellation_token.clone(), @@ -31,7 +34,9 @@ impl BitwardenDesktopAgent { let cloned_agent_state = agent_state.clone(); tokio::spawn(async move { - *cloned_agent_state.is_running.lock().await = true; + cloned_agent_state + .is_running + .store(true, std::sync::atomic::Ordering::Relaxed); let _ = ssh_agent::serve( stream, cloned_agent_state.clone(), @@ -39,7 +44,9 @@ impl BitwardenDesktopAgent { cloned_agent_state.cancellation_token.clone(), ) .await; - *cloned_agent_state.is_running.lock().await = false; + cloned_agent_state + .is_running + .store(false, std::sync::atomic::Ordering::Relaxed); }); Ok(agent_state) } diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 0eaba197919..b884829e77d 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -67,7 +67,7 @@ export declare namespace sshagent { status: SshKeyImportStatus sshKey?: SshKey } - export function serve(callback: (err: Error | null, arg0: string, arg1: boolean) => any): Promise + export function serve(callback: (err: Error | null, arg0: string | undefined | null, arg1: boolean, arg2: string) => any): Promise export function stop(agentState: SshAgentState): void export function isRunning(agentState: SshAgentState): boolean export function setKeys(agentState: SshAgentState, newKeys: Array): void diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 5037108afd7..a7e2144b1dc 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -247,30 +247,28 @@ pub mod sshagent { #[napi] pub async fn serve( - callback: ThreadsafeFunction<(String, bool), CalleeHandled>, + callback: ThreadsafeFunction<(Option, bool, String), CalleeHandled>, ) -> napi::Result { let (auth_request_tx, mut auth_request_rx) = - tokio::sync::mpsc::channel::<(u32, (String, bool))>(32); + tokio::sync::mpsc::channel::(32); let (auth_response_tx, auth_response_rx) = tokio::sync::broadcast::channel::<(u32, bool)>(32); let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx)); tokio::spawn(async move { let _ = auth_response_rx; - while let Some((request_id, (cipher_uuid, is_list_request))) = - auth_request_rx.recv().await - { - let cloned_request_id = request_id.clone(); - let cloned_cipher_uuid = cipher_uuid.clone(); + while let Some(request) = auth_request_rx.recv().await { let cloned_response_tx_arc = auth_response_tx_arc.clone(); let cloned_callback = callback.clone(); tokio::spawn(async move { - let request_id = cloned_request_id; - let cipher_uuid = cloned_cipher_uuid; let auth_response_tx_arc = cloned_response_tx_arc; let callback = cloned_callback; let promise_result: Result, napi::Error> = callback - .call_async(Ok((cipher_uuid, is_list_request))) + .call_async(Ok(( + request.cipher_id, + request.is_list, + request.process_name, + ))) .await; match promise_result { Ok(promise_result) => match promise_result.await { @@ -278,7 +276,7 @@ pub mod sshagent { let _ = auth_response_tx_arc .lock() .await - .send((request_id, result)) + .send((request.request_id, result)) .expect("should be able to send auth response to agent"); } Err(e) => { @@ -286,7 +284,7 @@ pub mod sshagent { let _ = auth_response_tx_arc .lock() .await - .send((request_id, false)) + .send((request.request_id, false)) .expect("should be able to send auth response to agent"); } }, @@ -295,7 +293,7 @@ pub mod sshagent { let _ = auth_response_tx_arc .lock() .await - .send((request_id, false)) + .send((request.request_id, false)) .expect("should be able to send auth response to agent"); } } diff --git a/apps/desktop/desktop_native/napi/src/registry/windows.rs b/apps/desktop/desktop_native/napi/src/registry/windows.rs index 481dfb5dc49..aeb381dafda 100644 --- a/apps/desktop/desktop_native/napi/src/registry/windows.rs +++ b/apps/desktop/desktop_native/napi/src/registry/windows.rs @@ -13,7 +13,7 @@ pub fn create_key(key: &str, subkey: &str, value: &str) -> Result<()> { let key = convert_key(key)?; let subkey = key.create(subkey)?; - + const DEFAULT: &str = ""; subkey.set_string(DEFAULT, value)?; diff --git a/apps/desktop/src/platform/main/main-ssh-agent.service.ts b/apps/desktop/src/platform/main/main-ssh-agent.service.ts index 9141e30d820..8858134a6be 100644 --- a/apps/desktop/src/platform/main/main-ssh-agent.service.ts +++ b/apps/desktop/src/platform/main/main-ssh-agent.service.ts @@ -29,7 +29,7 @@ export class MainSshAgentService { init() { // handle sign request passing to UI sshagent - .serve(async (err: Error, cipherId: string, isListRequest: boolean) => { + .serve(async (err: Error, cipherId: string, isListRequest: boolean, processName: string) => { // clear all old (> SIGN_TIMEOUT) requests this.requestResponses = this.requestResponses.filter( (response) => response.timestamp > new Date(Date.now() - this.SIGN_TIMEOUT), @@ -41,6 +41,7 @@ export class MainSshAgentService { cipherId, isListRequest, requestId: id_for_this_request, + processName, }); const result = await firstValueFrom( diff --git a/apps/desktop/src/platform/services/ssh-agent.service.ts b/apps/desktop/src/platform/services/ssh-agent.service.ts index 9dc7abeca01..651e67e9467 100644 --- a/apps/desktop/src/platform/services/ssh-agent.service.ts +++ b/apps/desktop/src/platform/services/ssh-agent.service.ts @@ -122,6 +122,10 @@ export class SshAgentService implements OnDestroy { const cipherId = message.cipherId as string; const isListRequest = message.isListRequest as boolean; const requestId = message.requestId as number; + let application = message.processName as string; + if (application == "") { + application = this.i18nService.t("unknownApplication"); + } if (isListRequest) { const sshCiphers = ciphers.filter( @@ -151,7 +155,7 @@ export class SshAgentService implements OnDestroy { const dialogRef = ApproveSshRequestComponent.open( this.dialogService, cipher.name, - this.i18nService.t("unknownApplication"), + application, ); const result = await firstValueFrom(dialogRef.closed); From d4fcb5852a52e29400f74e0bfceac76d979658d7 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Wed, 11 Dec 2024 13:20:32 +0100 Subject: [PATCH 2/8] fix: text-drag directive ts-strict error (#12346) --- libs/angular/src/directives/text-drag.directive.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/angular/src/directives/text-drag.directive.ts b/libs/angular/src/directives/text-drag.directive.ts index da3e70d1de2..443fbdac157 100644 --- a/libs/angular/src/directives/text-drag.directive.ts +++ b/libs/angular/src/directives/text-drag.directive.ts @@ -17,6 +17,6 @@ export class TextDragDirective { @HostListener("dragstart", ["$event"]) onDragStart(event: DragEvent) { - event.dataTransfer.setData("text", this.data); + event.dataTransfer?.setData("text", this.data); } } From b502e2bc251dfbfee2c5220ab3ccd4629ddcbbd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 11 Dec 2024 14:47:49 +0000 Subject: [PATCH 3/8] [PM-15154] Domain verification copy update (#12217) * refactor: update domain verification terminology to claimed domains * feat: add description for claimed domains in domain verification * Add informational link to claimed domains description in domain verification component * Update domain verification references to claimed domains in organization layout and SSO component * Add validation message for invalid domain name format in domain verification * Add domain claim messages and descriptions to localization files * Update domain verification navigation text based on feature flag * Update domain verification dialog to support account deprovisioning feature flag * Update domain verification component to support account deprovisioning feature flag * Refactor domain verification dialog to use synchronous feature flag for account deprovisioning * Refactor domain verification routing to resolve title based on account deprovisioning feature flag * Update SSO component to conditionally display domain verification link based on account deprovisioning feature flag * Update event service to conditionally display domain verification messages based on account deprovisioning feature flag * Update domain verification warning message * Refactor domain verification navigation text handling based on account deprovisioning feature flag * Refactor domain verification dialog to use observable for account deprovisioning feature flag * Refactor domain verification component to use observable for account deprovisioning feature flag --- .../organization-layout.component.html | 2 +- .../layouts/organization-layout.component.ts | 9 +++ apps/web/src/app/core/event.service.ts | 17 +++++- apps/web/src/locales/en/messages.json | 61 ++++++++++++++++++- .../domain-add-edit-dialog.component.html | 40 +++++++++--- .../domain-add-edit-dialog.component.ts | 58 ++++++++++++------ .../domain-verification.component.html | 29 ++++++++- .../domain-verification.component.ts | 22 +++++-- .../organizations-routing.module.ts | 13 +++- .../src/app/auth/sso/sso.component.html | 5 +- .../bit-web/src/app/auth/sso/sso.component.ts | 11 +++- 11 files changed, 223 insertions(+), 44 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index fa4d027d0f6..8387c53e5e3 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -98,7 +98,7 @@ *ngIf="canAccessExport$ | async" > diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index 91c965658a3..6ead83b01d8 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts @@ -23,6 +23,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { ProductTierType } from "@bitwarden/common/billing/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { getById } from "@bitwarden/common/platform/misc"; import { BannerModule, IconModule } from "@bitwarden/components"; @@ -49,6 +50,7 @@ export class OrganizationLayoutComponent implements OnInit { protected readonly logo = AdminConsoleLogo; protected orgFilter = (org: Organization) => canAccessOrgAdmin(org); + protected domainVerificationNavigationTextKey: string; protected integrationPageEnabled$: Observable; @@ -67,6 +69,7 @@ export class OrganizationLayoutComponent implements OnInit { private configService: ConfigService, private policyService: PolicyService, private providerService: ProviderService, + private i18nService: I18nService, ) {} async ngOnInit() { @@ -116,6 +119,12 @@ export class OrganizationLayoutComponent implements OnInit { org.productTierType === ProductTierType.Enterprise && featureFlagEnabled, ), ); + + this.domainVerificationNavigationTextKey = (await this.configService.getFeatureFlag( + FeatureFlag.AccountDeprovisioning, + )) + ? "claimedDomains" + : "domainVerification"; } canShowVaultTab(organization: Organization): boolean { diff --git a/apps/web/src/app/core/event.service.ts b/apps/web/src/app/core/event.service.ts index 412423a3a24..aedad9b26ea 100644 --- a/apps/web/src/app/core/event.service.ts +++ b/apps/web/src/app/core/event.service.ts @@ -6,7 +6,9 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { DeviceType, EventType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EventResponse } from "@bitwarden/common/models/response/event.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @Injectable() @@ -16,6 +18,7 @@ export class EventService { constructor( private i18nService: I18nService, policyService: PolicyService, + private configService: ConfigService, ) { policyService.policies$.subscribe((policies) => { this.policies = policies; @@ -451,10 +454,20 @@ export class EventService { msg = humanReadableMsg = this.i18nService.t("removedDomain", ev.domainName); break; case EventType.OrganizationDomain_Verified: - msg = humanReadableMsg = this.i18nService.t("domainVerifiedEvent", ev.domainName); + msg = humanReadableMsg = this.i18nService.t( + (await this.configService.getFeatureFlag(FeatureFlag.AccountDeprovisioning)) + ? "domainClaimedEvent" + : "domainVerifiedEvent", + ev.domainName, + ); break; case EventType.OrganizationDomain_NotVerified: - msg = humanReadableMsg = this.i18nService.t("domainNotVerifiedEvent", ev.domainName); + msg = humanReadableMsg = this.i18nService.t( + (await this.configService.getFeatureFlag(FeatureFlag.AccountDeprovisioning)) + ? "domainNotClaimedEvent" + : "domainNotVerifiedEvent", + ev.domainName, + ); break; // Secrets Manager case EventType.Secret_Retrieved: diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 06728929912..b1203230688 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9801,8 +9801,8 @@ "selfHostingTitleProper": { "message": "Self-Hosting" }, - "verified-domain-single-org-warning" : { - "message": "Verifying a domain will turn on the single organization policy." + "claim-domain-single-org-warning" : { + "message": "Claiming a domain will turn on the single organization policy." }, "single-org-revoked-user-warning": { "message": "Non-compliant members will be revoked. Administrators can restore members once they leave all other organizations." @@ -9902,5 +9902,62 @@ }, "removeMembers": { "message": "Remove members" + }, + "claimedDomains": { + "message": "Claimed domains" + }, + "claimDomain": { + "message": "Claim domain" + }, + "reclaimDomain": { + "message": "Reclaim domain" + }, + "claimDomainNameInputHint": { + "message": "Example: mydomain.com. Subdomains require separate entries to be claimed." + }, + "automaticClaimedDomains": { + "message": "Automatic Claimed Domains" + }, + "automaticDomainClaimProcess": { + "message": "Bitwarden will attempt to claim the domain 3 times during the first 72 hours. If the domain can’t be claimed, check the DNS record in your host and manually claim. The domain will be removed from your organization in 7 days if it is not claimed." + }, + "domainNotClaimed": { + "message": "$DOMAIN$ not claimed. Check your DNS records.", + "placeholders": { + "DOMAIN": { + "content": "$1", + "example": "bitwarden.com" + } + } + }, + "domainStatusClaimed": { + "message": "Claimed" + }, + "domainStatusUnderVerification": { + "message": "Under verification" + }, + "claimedDomainsDesc": { + "message": "Claim a domain to own all member accounts whose email address matches the domain. Members will be able to skip the SSO identifier when logging in. Administrators will also be able to delete member accounts." + }, + "invalidDomainNameClaimMessage": { + "message": "Input is not a valid format. Format: mydomain.com. Subdomains require separate entries to be claimed." + }, + "domainClaimedEvent": { + "message": "$DOMAIN$ claimed", + "placeholders": { + "DOMAIN": { + "content": "$1", + "example": "bitwarden.com" + } + } + }, + "domainNotClaimedEvent": { + "message": "$DOMAIN$ not claimed", + "placeholders": { + "DOMAIN": { + "content": "$1", + "example": "bitwarden.com" + } + } } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html index 15120eed92a..7226c957598 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html @@ -6,24 +6,37 @@ {{ "newDomain" | i18n }} - {{ "verifyDomain" | i18n }} + + {{ + ((accountDeprovisioningEnabled$ | async) ? "claimDomain" : "verifyDomain") | i18n + }} {{ data.orgDomain.domainName }} {{ - "domainStatusUnverified" | i18n + ((accountDeprovisioningEnabled$ | async) + ? "domainStatusUnderVerification" + : "domainStatusUnverified" + ) | i18n }} {{ - "domainStatusVerified" | i18n + ((accountDeprovisioningEnabled$ | async) ? "domainStatusClaimed" : "domainStatusVerified") + | i18n }}
{{ "domainName" | i18n }} - {{ "domainNameInputHint" | i18n }} + {{ + ((accountDeprovisioningEnabled$ | async) + ? "claimDomainNameInputHint" + : "domainNameInputHint" + ) | i18n + }} @@ -42,18 +55,29 @@ - {{ "automaticDomainVerificationProcess" | i18n }} + {{ + ((accountDeprovisioningEnabled$ | async) + ? "automaticDomainClaimProcess" + : "automaticDomainVerificationProcess" + ) | i18n + }}
+

+ {{ "claimedDomainsDesc" | i18n }} + + + +

+ {{ - "domainStatusUnverified" | i18n + ((accountDeprovisioningEnabled$ | async) + ? "domainStatusUnderVerification" + : "domainStatusUnverified" + ) | i18n }} {{ - "domainStatusVerified" | i18n + ((accountDeprovisioningEnabled$ | async) + ? "domainStatusClaimed" + : "domainStatusVerified" + ) | i18n }} @@ -70,7 +90,10 @@ type="button" > - {{ "verifyDomain" | i18n }} + {{ + ((accountDeprovisioningEnabled$ | async) ? "claimDomain" : "verifyDomain") + | i18n + }} + + + + + {{ "loading" | i18n }} + + + +

{{ "noClientsInList" | i18n }}

+ + + + {{ "name" | i18n }} + {{ "numberOfUsers" | i18n }} + {{ "billingPlan" | i18n }} + + + + + + + + {{ row.organizationName }} + + + {{ row.userCount }} + / {{ row.seats }} + + + {{ row.plan }} + + + + + + + +
diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.ts new file mode 100644 index 00000000000..ba56ce872b2 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.ts @@ -0,0 +1,167 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormControl } from "@angular/forms"; +import { ActivatedRoute, Router, RouterModule } from "@angular/router"; +import { firstValueFrom, from, map } from "rxjs"; +import { debounceTime, first, switchMap } from "rxjs/operators"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; +import { PlanType } from "@bitwarden/common/billing/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { + AvatarModule, + DialogService, + TableDataSource, + TableModule, + ToastService, +} from "@bitwarden/components"; +import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; +import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; + +import { WebProviderService } from "../services/web-provider.service"; + +import { AddOrganizationComponent } from "./add-organization.component"; + +const DisallowedPlanTypes = [ + PlanType.Free, + PlanType.FamiliesAnnually2019, + PlanType.FamiliesAnnually, + PlanType.TeamsStarter2023, + PlanType.TeamsStarter, +]; + +@Component({ + templateUrl: "vnext-clients.component.html", + standalone: true, + imports: [ + SharedOrganizationModule, + HeaderModule, + CommonModule, + JslibModule, + AvatarModule, + RouterModule, + TableModule, + ], +}) +export class vNextClientsComponent { + providerId: string; + addableOrganizations: Organization[]; + loading = true; + manageOrganizations = false; + showAddExisting = false; + dataSource: TableDataSource = + new TableDataSource(); + protected searchControl = new FormControl("", { nonNullable: true }); + + constructor( + private router: Router, + private providerService: ProviderService, + private apiService: ApiService, + private organizationService: OrganizationService, + private organizationApiService: OrganizationApiServiceAbstraction, + private activatedRoute: ActivatedRoute, + private dialogService: DialogService, + private i18nService: I18nService, + private toastService: ToastService, + private validationService: ValidationService, + private webProviderService: WebProviderService, + ) { + this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => { + this.searchControl.setValue(queryParams.search); + }); + + this.activatedRoute.parent.params + .pipe( + switchMap((params) => { + this.providerId = params.providerId; + return this.providerService.get$(this.providerId).pipe( + map((provider) => provider?.providerStatus === ProviderStatusType.Billable), + map((isBillable) => { + if (isBillable) { + return from( + this.router.navigate(["../manage-client-organizations"], { + relativeTo: this.activatedRoute, + }), + ); + } else { + return from(this.load()); + } + }), + ); + }), + takeUntilDestroyed(), + ) + .subscribe(); + + this.searchControl.valueChanges + .pipe(debounceTime(200), takeUntilDestroyed()) + .subscribe((searchText) => { + this.dataSource.filter = (data) => + data.organizationName.toLowerCase().indexOf(searchText.toLowerCase()) > -1; + }); + } + + async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: organization.organizationName, + content: { key: "detachOrganizationConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + await this.webProviderService.detachOrganization(this.providerId, organization.id); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("detachedOrganization", organization.organizationName), + }); + await this.load(); + } catch (e) { + this.validationService.showError(e); + } + } + + async load() { + const response = await this.apiService.getProviderClients(this.providerId); + const clients = response.data != null && response.data.length > 0 ? response.data : []; + this.dataSource.data = clients; + this.manageOrganizations = + (await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin; + const candidateOrgs = (await this.organizationService.getAll()).filter( + (o) => o.isOwner && o.providerId == null, + ); + const allowedOrgsIds = await Promise.all( + candidateOrgs.map((o) => this.organizationApiService.get(o.id)), + ).then((orgs) => + orgs.filter((o) => !DisallowedPlanTypes.includes(o.planType)).map((o) => o.id), + ); + this.addableOrganizations = candidateOrgs.filter((o) => allowedOrgsIds.includes(o.id)); + + this.showAddExisting = this.addableOrganizations.length !== 0; + this.loading = false; + } + + async addExistingOrganization() { + const dialogRef = AddOrganizationComponent.open(this.dialogService, { + providerId: this.providerId, + organizations: this.addableOrganizations, + }); + + if (await firstValueFrom(dialogRef.closed)) { + await this.load(); + } + } +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts index 00c944e69bb..09276263332 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts @@ -2,8 +2,10 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { authGuard } from "@bitwarden/angular/auth/guards"; +import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; import { AnonLayoutWrapperComponent } from "@bitwarden/auth/angular"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component"; import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-layout.component"; @@ -12,10 +14,12 @@ import { ProviderSubscriptionComponent, hasConsolidatedBilling, ProviderBillingHistoryComponent, + vNextManageClientsComponent, } from "../../billing/providers"; import { ClientsComponent } from "./clients/clients.component"; import { CreateOrganizationComponent } from "./clients/create-organization.component"; +import { vNextClientsComponent } from "./clients/vnext-clients.component"; import { providerPermissionsGuard } from "./guards/provider-permissions.guard"; import { AcceptProviderComponent } from "./manage/accept-provider.component"; import { EventsComponent } from "./manage/events.component"; @@ -82,13 +86,25 @@ const routes: Routes = [ children: [ { path: "", pathMatch: "full", redirectTo: "clients" }, { path: "clients/create", component: CreateOrganizationComponent }, - { path: "clients", component: ClientsComponent, data: { titleId: "clients" } }, - { - path: "manage-client-organizations", - canActivate: [hasConsolidatedBilling], - component: ManageClientsComponent, - data: { titleId: "clients" }, - }, + ...featureFlaggedRoute({ + defaultComponent: ClientsComponent, + flaggedComponent: vNextClientsComponent, + featureFlag: FeatureFlag.PM12443RemovePagingLogic, + routeOptions: { + path: "clients", + data: { titleId: "clients" }, + }, + }), + ...featureFlaggedRoute({ + defaultComponent: ManageClientsComponent, + flaggedComponent: vNextManageClientsComponent, + featureFlag: FeatureFlag.PM12443RemovePagingLogic, + routeOptions: { + path: "manage-client-organizations", + data: { titleId: "clients" }, + canActivate: [hasConsolidatedBilling], + }, + }), { path: "manage", children: [ diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts index ae7bf487f99..f8b344372ef 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts @@ -3,3 +3,4 @@ export * from "./manage-clients.component"; export * from "./manage-client-name-dialog.component"; export * from "./manage-client-subscription-dialog.component"; export * from "./no-clients.component"; +export * from "./vnext-manage-clients.component"; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.html new file mode 100644 index 00000000000..c54965bbdb6 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.html @@ -0,0 +1,83 @@ + + + + + {{ "addNewOrganization" | i18n }} + + + + + + {{ "loading" | i18n }} + + + + + + {{ "client" | i18n }} + {{ "assigned" | i18n }} + {{ "used" | i18n }} + {{ "remaining" | i18n }} + {{ "billingPlan" | i18n }} + + + + + + + + + + + {{ row.seats }} + + + {{ row.occupiedSeats }} + + + {{ row.remainingSeats }} + + + {{ row.plan }} + + + + + + + + + + + +
+ +
+
diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.ts new file mode 100644 index 00000000000..5ee7817f34e --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.ts @@ -0,0 +1,201 @@ +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormControl } from "@angular/forms"; +import { ActivatedRoute, Router } from "@angular/router"; +import { firstValueFrom, from, lastValueFrom, map } from "rxjs"; +import { debounceTime, first, switchMap } from "rxjs/operators"; + +import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; +import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { + AvatarModule, + DialogService, + TableDataSource, + TableModule, + ToastService, +} from "@bitwarden/components"; +import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; +import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; + +import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; + +import { + CreateClientDialogResultType, + openCreateClientDialog, +} from "./create-client-dialog.component"; +import { + ManageClientNameDialogResultType, + openManageClientNameDialog, +} from "./manage-client-name-dialog.component"; +import { + ManageClientSubscriptionDialogResultType, + openManageClientSubscriptionDialog, +} from "./manage-client-subscription-dialog.component"; +import { vNextNoClientsComponent } from "./vnext-no-clients.component"; + +@Component({ + templateUrl: "vnext-manage-clients.component.html", + standalone: true, + imports: [ + AvatarModule, + TableModule, + HeaderModule, + SharedOrganizationModule, + vNextNoClientsComponent, + ], +}) +export class vNextManageClientsComponent { + providerId: string; + provider: Provider; + loading = true; + isProviderAdmin = false; + dataSource: TableDataSource = + new TableDataSource(); + + protected searchControl = new FormControl("", { nonNullable: true }); + protected plans: PlanResponse[]; + + constructor( + private billingApiService: BillingApiServiceAbstraction, + private providerService: ProviderService, + private router: Router, + private activatedRoute: ActivatedRoute, + private dialogService: DialogService, + private i18nService: I18nService, + private toastService: ToastService, + private validationService: ValidationService, + private webProviderService: WebProviderService, + ) { + this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => { + this.searchControl.setValue(queryParams.search); + }); + + this.activatedRoute.parent.params + .pipe( + switchMap((params) => { + this.providerId = params.providerId; + return this.providerService.get$(this.providerId).pipe( + map((provider: Provider) => provider?.providerStatus === ProviderStatusType.Billable), + map((isBillable) => { + if (!isBillable) { + return from( + this.router.navigate(["../clients"], { + relativeTo: this.activatedRoute, + }), + ); + } else { + return from(this.load()); + } + }), + ); + }), + takeUntilDestroyed(), + ) + .subscribe(); + + this.searchControl.valueChanges + .pipe(debounceTime(200), takeUntilDestroyed()) + .subscribe((searchText) => { + this.dataSource.filter = (data) => + data.organizationName.toLowerCase().indexOf(searchText.toLowerCase()) > -1; + }); + } + + async load() { + this.provider = await firstValueFrom(this.providerService.get$(this.providerId)); + + this.isProviderAdmin = this.provider.type === ProviderUserType.ProviderAdmin; + + const clients = (await this.billingApiService.getProviderClientOrganizations(this.providerId)) + .data; + + clients.forEach((client) => (client.plan = client.plan.replace(" (Monthly)", ""))); + + this.dataSource.data = clients; + + this.plans = (await this.billingApiService.getPlans()).data; + + this.loading = false; + } + + createClient = async () => { + const reference = openCreateClientDialog(this.dialogService, { + data: { + providerId: this.providerId, + plans: this.plans, + }, + }); + + const result = await lastValueFrom(reference.closed); + + if (result === CreateClientDialogResultType.Submitted) { + await this.load(); + } + }; + + manageClientName = async (organization: ProviderOrganizationOrganizationDetailsResponse) => { + const dialogRef = openManageClientNameDialog(this.dialogService, { + data: { + providerId: this.providerId, + organization: { + id: organization.id, + name: organization.organizationName, + seats: organization.seats, + }, + }, + }); + + const result = await firstValueFrom(dialogRef.closed); + + if (result === ManageClientNameDialogResultType.Submitted) { + await this.load(); + } + }; + + manageClientSubscription = async ( + organization: ProviderOrganizationOrganizationDetailsResponse, + ) => { + const dialogRef = openManageClientSubscriptionDialog(this.dialogService, { + data: { + organization, + provider: this.provider, + }, + }); + + const result = await firstValueFrom(dialogRef.closed); + + if (result === ManageClientSubscriptionDialogResultType.Submitted) { + await this.load(); + } + }; + + async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: organization.organizationName, + content: { key: "detachOrganizationConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + await this.webProviderService.detachOrganization(this.providerId, organization.id); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("detachedOrganization", organization.organizationName), + }); + await this.load(); + } catch (e) { + this.validationService.showError(e); + } + } +} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-no-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-no-clients.component.ts new file mode 100644 index 00000000000..5ad19945c51 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-no-clients.component.ts @@ -0,0 +1,50 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; + +import { svgIcon } from "@bitwarden/components"; +import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; + +const gearIcon = svgIcon` + + + + + + + + + + + + + + + + +`; + +@Component({ + selector: "app-no-clients", + standalone: true, + imports: [SharedOrganizationModule], + template: `
+ +

{{ "noClients" | i18n }}

+ + + {{ "addNewOrganization" | i18n }} + +
`, +}) +export class vNextNoClientsComponent { + icon = gearIcon; + @Input() showAddOrganizationButton = true; + @Output() addNewOrganizationClicked = new EventEmitter(); + + addNewOrganization = () => this.addNewOrganizationClicked.emit(); +} diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index f79ebf8aa55..6597c97b641 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -40,6 +40,7 @@ export enum FeatureFlag { DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship", MacOsNativeCredentialSync = "macos-native-credential-sync", PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission", + PM12443RemovePagingLogic = "pm-12443-remove-paging-logic", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -90,6 +91,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE, [FeatureFlag.MacOsNativeCredentialSync]: FALSE, [FeatureFlag.PM11360RemoveProviderExportPermission]: FALSE, + [FeatureFlag.PM12443RemovePagingLogic]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; From 92a620dd9c06c46127164d3c7a103aeafff92708 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 11 Dec 2024 07:10:06 -0800 Subject: [PATCH 5/8] [BEEEP/PM-10534] Add snap biometric support (#12187) * Add snap biometric support * Fix linting * Remove unused message * Disable snap browser integration again --- apps/desktop/electron-builder.json | 11 ++++++++++- apps/desktop/package.json | 2 +- .../resources/com.bitwarden.desktop.policy | 16 ++++++++++++++++ .../biometrics/biometric.unix.main.ts | 10 +++++++--- apps/desktop/src/locales/en/messages.json | 3 --- 5 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 apps/desktop/resources/com.bitwarden.desktop.policy diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 9b894b0bfc7..38f11a97a8b 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -241,7 +241,16 @@ "autoStart": true, "base": "core22", "confinement": "strict", - "plugs": ["default", "network-bind", "password-manager-service"], + "plugs": [ + "default", + "network-bind", + "password-manager-service", + { + "polkit": { + "action-prefix": "com.bitwarden.Bitwarden" + } + } + ], "stagePackages": ["default"] }, "protocols": [ diff --git a/apps/desktop/package.json b/apps/desktop/package.json index eab9a7d7119..f546563ed18 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -35,7 +35,7 @@ "clean:dist": "rimraf ./dist", "pack:dir": "npm run clean:dist && electron-builder --dir -p never", "pack:lin:flatpak": "npm run clean:dist && electron-builder --dir -p never && flatpak-builder --repo=build/.repo build/.flatpak ./resources/com.bitwarden.desktop.devel.yaml --install-deps-from=flathub --force-clean && flatpak build-bundle ./build/.repo/ ./dist/com.bitwarden.desktop.flatpak com.bitwarden.desktop", - "pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never", + "pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && mksquashfs ./dist/tmp-snap/ $SNAP_FILE -noappend -comp lzo -no-fragments && rm -rf ./dist/tmp-snap/", "pack:mac": "npm run clean:dist && electron-builder --mac --universal -p never", "pack:mac:arm64": "npm run clean:dist && electron-builder --mac --arm64 -p never", "pack:mac:mas": "npm run clean:dist && electron-builder --mac mas --universal -p never", diff --git a/apps/desktop/resources/com.bitwarden.desktop.policy b/apps/desktop/resources/com.bitwarden.desktop.policy new file mode 100644 index 00000000000..e48bc6b8fbb --- /dev/null +++ b/apps/desktop/resources/com.bitwarden.desktop.policy @@ -0,0 +1,16 @@ + + + + + + Unlock Bitwarden + Authenticate to unlock Bitwarden + + no + no + auth_self + + + diff --git a/apps/desktop/src/key-management/biometrics/biometric.unix.main.ts b/apps/desktop/src/key-management/biometrics/biometric.unix.main.ts index 771f1ea3a1c..f2bcf62e03e 100644 --- a/apps/desktop/src/key-management/biometrics/biometric.unix.main.ts +++ b/apps/desktop/src/key-management/biometrics/biometric.unix.main.ts @@ -87,8 +87,8 @@ export default class BiometricUnixMain implements OsBiometricService { } async authenticateBiometric(): Promise { - const hwnd = this.windowMain.win.getNativeWindowHandle(); - return await biometrics.prompt(hwnd, this.i18nservice.t("polkitConsentMessage")); + const hwnd = Buffer.from(""); + return await biometrics.prompt(hwnd, ""); } async osSupportsBiometric(): Promise { @@ -98,10 +98,14 @@ export default class BiometricUnixMain implements OsBiometricService { // This could be dynamically detected on dbus in the future. // We should check if a libsecret implementation is available on the system // because otherwise we cannot offlod the protected userkey to secure storage. - return (await passwords.isAvailable()) && !isSnapStore(); + return await passwords.isAvailable(); } async osBiometricsNeedsSetup(): Promise { + if (isSnapStore()) { + return false; + } + // check whether the polkit policy is loaded via dbus call to polkit return !(await biometrics.available()); } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index e4c235dada9..f8f81a5ac2c 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1734,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, From 8dd904f4b7ad767a24839a18b878703514be1f87 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Wed, 11 Dec 2024 19:08:14 -0500 Subject: [PATCH 7/8] fix ts strict errors (#12355) --- .../clients/vnext-clients.component.ts | 10 +++++----- .../clients/vnext-manage-clients.component.ts | 20 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.ts index ba56ce872b2..2be38477d4c 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.ts @@ -53,8 +53,8 @@ const DisallowedPlanTypes = [ ], }) export class vNextClientsComponent { - providerId: string; - addableOrganizations: Organization[]; + providerId: string = ""; + addableOrganizations: Organization[] = []; loading = true; manageOrganizations = false; showAddExisting = false; @@ -79,8 +79,8 @@ export class vNextClientsComponent { this.searchControl.setValue(queryParams.search); }); - this.activatedRoute.parent.params - .pipe( + this.activatedRoute.parent?.params + ?.pipe( switchMap((params) => { this.providerId = params.providerId; return this.providerService.get$(this.providerId).pipe( @@ -125,7 +125,7 @@ export class vNextClientsComponent { await this.webProviderService.detachOrganization(this.providerId, organization.id); this.toastService.showToast({ variant: "success", - title: null, + title: "", message: this.i18nService.t("detachedOrganization", organization.organizationName), }); await this.load(); diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.ts index 5ee7817f34e..4c0837d6da2 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.ts @@ -51,15 +51,15 @@ import { vNextNoClientsComponent } from "./vnext-no-clients.component"; ], }) export class vNextManageClientsComponent { - providerId: string; - provider: Provider; + providerId: string = ""; + provider: Provider | undefined; loading = true; isProviderAdmin = false; dataSource: TableDataSource = new TableDataSource(); protected searchControl = new FormControl("", { nonNullable: true }); - protected plans: PlanResponse[]; + protected plans: PlanResponse[] = []; constructor( private billingApiService: BillingApiServiceAbstraction, @@ -76,8 +76,8 @@ export class vNextManageClientsComponent { this.searchControl.setValue(queryParams.search); }); - this.activatedRoute.parent.params - .pipe( + this.activatedRoute.parent?.params + ?.pipe( switchMap((params) => { this.providerId = params.providerId; return this.providerService.get$(this.providerId).pipe( @@ -110,12 +110,12 @@ export class vNextManageClientsComponent { async load() { this.provider = await firstValueFrom(this.providerService.get$(this.providerId)); - this.isProviderAdmin = this.provider.type === ProviderUserType.ProviderAdmin; + this.isProviderAdmin = this.provider?.type === ProviderUserType.ProviderAdmin; const clients = (await this.billingApiService.getProviderClientOrganizations(this.providerId)) .data; - clients.forEach((client) => (client.plan = client.plan.replace(" (Monthly)", ""))); + clients.forEach((client) => (client.plan = client.plan?.replace(" (Monthly)", ""))); this.dataSource.data = clients; @@ -146,7 +146,7 @@ export class vNextManageClientsComponent { organization: { id: organization.id, name: organization.organizationName, - seats: organization.seats, + seats: organization.seats ? organization.seats : 0, }, }, }); @@ -164,7 +164,7 @@ export class vNextManageClientsComponent { const dialogRef = openManageClientSubscriptionDialog(this.dialogService, { data: { organization, - provider: this.provider, + provider: this.provider!, }, }); @@ -190,7 +190,7 @@ export class vNextManageClientsComponent { await this.webProviderService.detachOrganization(this.providerId, organization.id); this.toastService.showToast({ variant: "success", - title: null, + title: "", message: this.i18nService.t("detachedOrganization", organization.organizationName), }); await this.load(); From cecf1f2506a69b8a6edc16be19e772531785a5b7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:26:05 +0100 Subject: [PATCH 8/8] [deps] Platform: Update electron to v33 - abandoned (#11580) * [deps] Platform: Update electron to v33 * fix: remove event from minimize * chore: update electron version in `electron-builder.json` --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Andreas Coroiu Co-authored-by: Bernd Schoolmann --- apps/desktop/electron-builder.json | 2 +- apps/desktop/src/main/tray.main.ts | 3 +-- package-lock.json | 14 +++++++------- package.json | 2 +- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 38f11a97a8b..898ad086b29 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -20,7 +20,7 @@ "**/node_modules/@bitwarden/desktop-napi/index.js", "**/node_modules/@bitwarden/desktop-napi/desktop_napi.${platform}-${arch}*.node" ], - "electronVersion": "32.1.1", + "electronVersion": "33.2.1", "generateUpdatesFilesForAllChannels": true, "publish": { "provider": "generic", diff --git a/apps/desktop/src/main/tray.main.ts b/apps/desktop/src/main/tray.main.ts index 641af8db0ad..52a8615a1da 100644 --- a/apps/desktop/src/main/tray.main.ts +++ b/apps/desktop/src/main/tray.main.ts @@ -64,9 +64,8 @@ export class TrayMain { } setupWindowListeners(win: BrowserWindow) { - win.on("minimize", async (e: Event) => { + win.on("minimize", async () => { if (await firstValueFrom(this.desktopSettingsService.minimizeToTray$)) { - e.preventDefault(); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.hideToTray(); diff --git a/package-lock.json b/package-lock.json index 64a7c926ca2..2da7d9e6255 100644 --- a/package-lock.json +++ b/package-lock.json @@ -132,7 +132,7 @@ "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "7.1.2", - "electron": "32.1.1", + "electron": "33.2.1", "electron-builder": "24.13.3", "electron-log": "5.2.4", "electron-reload": "2.0.0-alpha.1", @@ -15745,9 +15745,9 @@ } }, "node_modules/electron": { - "version": "32.1.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-32.1.1.tgz", - "integrity": "sha512-NlWvG6kXOJbZbELmzP3oV7u50I3NHYbCeh+AkUQ9vGyP7b74cFMx9HdTzejODeztW1jhr3SjIBbUZzZ45zflfQ==", + "version": "33.2.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-33.2.1.tgz", + "integrity": "sha512-SG/nmSsK9Qg1p6wAW+ZfqU+AV8cmXMTIklUL18NnOKfZLlum4ZsDoVdmmmlL39ZmeCaq27dr7CgslRPahfoVJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -15986,9 +15986,9 @@ } }, "node_modules/electron/node_modules/@types/node": { - "version": "20.17.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.6.tgz", - "integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==", + "version": "20.17.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.8.tgz", + "integrity": "sha512-ahz2g6/oqbKalW9sPv6L2iRbhLnojxjYWspAqhjvqSWBgGebEJT5GvRmk0QXPj3sbC6rU0GTQjPLQkmR8CObvA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 5573332db1a..aa567f18df6 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "7.1.2", - "electron": "32.1.1", + "electron": "33.2.1", "electron-builder": "24.13.3", "electron-log": "5.2.4", "electron-reload": "2.0.0-alpha.1",