refactor: use secret agent for managing passwords
This commit is contained in:
parent
a2f5a6daca
commit
d9b0a6944a
10 changed files with 1611 additions and 377 deletions
|
|
@ -18,6 +18,7 @@ cosmic-bg-config.workspace = true
|
|||
cosmic-comp-config = { workspace = true, optional = true }
|
||||
cosmic-config.workspace = true
|
||||
cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
|
||||
nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
|
||||
cosmic-idle-config.workspace = true
|
||||
cosmic-panel-config = { workspace = true, optional = true }
|
||||
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", optional = true }
|
||||
|
|
@ -164,6 +165,7 @@ page-networking = [
|
|||
"dep:cosmic-settings-network-manager-subscription",
|
||||
"xdg-portal",
|
||||
"dep:cosmic-dbus-networkmanager",
|
||||
"dep:nm-secret-agent-manager",
|
||||
"dep:zbus",
|
||||
]
|
||||
page-power = ["dep:upower_dbus", "dep:zbus"]
|
||||
|
|
|
|||
|
|
@ -16,8 +16,11 @@ use cosmic_dbus_networkmanager::{
|
|||
use cosmic_settings_network_manager_subscription as network_manager;
|
||||
use cosmic_settings_page::{self as page, Section, section};
|
||||
use futures::StreamExt;
|
||||
use secure_string::SecureString;
|
||||
use slotmap::SlotMap;
|
||||
|
||||
pub type SecretSender = Arc<tokio::sync::Mutex<Option<tokio::sync::oneshot::Sender<SecureString>>>>;
|
||||
|
||||
static NM_CONNECTION_EDITOR: &str = "nm-connection-editor";
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
mod nmcli;
|
||||
pub mod nmcli;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
|
|
@ -13,6 +14,7 @@ use cosmic::{
|
|||
iced_core::text::Wrapping,
|
||||
widget::{self, icon},
|
||||
};
|
||||
use cosmic_settings_network_manager_subscription::nm_secret_agent::{self, PasswordFlag};
|
||||
use cosmic_settings_network_manager_subscription::{
|
||||
self as network_manager, NetworkManagerState, UUID, current_networks::ActiveConnectionInfo,
|
||||
};
|
||||
|
|
@ -20,6 +22,9 @@ use cosmic_settings_page::{self as page, Section, section};
|
|||
use futures::{FutureExt, StreamExt};
|
||||
use indexmap::IndexMap;
|
||||
use secure_string::SecureString;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::pages::networking::SecretSender;
|
||||
|
||||
pub type ConnectionId = Arc<str>;
|
||||
pub type InterfaceId = String;
|
||||
|
|
@ -36,6 +41,8 @@ pub enum Message {
|
|||
CancelDialog,
|
||||
/// Connect to a VPN with the given username and password
|
||||
ConnectWithPassword,
|
||||
/// Connect to a VPN with the given username and password
|
||||
RetryWithPassword,
|
||||
/// Deactivate a connection.
|
||||
Deactivate(ConnectionId),
|
||||
/// An error occurred.
|
||||
|
|
@ -46,6 +53,8 @@ pub enum Message {
|
|||
KnownConnections(IndexMap<UUID, ConnectionSettings>),
|
||||
/// An update from the network manager daemon
|
||||
NetworkManager(network_manager::Event),
|
||||
/// An update from the secret agent
|
||||
SecretAgent(network_manager::nm_secret_agent::Event),
|
||||
/// Successfully connected to the system dbus.
|
||||
NetworkManagerConnect(zbus::Connection),
|
||||
/// Updates the password text input
|
||||
|
|
@ -146,31 +155,17 @@ enum ConnectionType {
|
|||
Password,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
enum PasswordFlag {
|
||||
/// The system is responsible for providing and storing this secret.
|
||||
None = 0,
|
||||
/// A user-session secret agent is responsible for providing and storing
|
||||
/// this secret; when it is required, agents will be asked to provide it.
|
||||
AgentOwned = 1,
|
||||
/// This secret should not be saved but should be requested from the user
|
||||
/// each time it is required. This flag should be used for One-Time-Pad
|
||||
/// secrets, PIN codes from hardware tokens, or if the user simply does not
|
||||
/// want to save the secret.
|
||||
NotSaved = 2,
|
||||
/// in some situations it cannot be automatically determined that a secret is required or not. This flag hints that the secret is not required and should not be requested from the user.
|
||||
NotRequired = 4,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum VpnDialog {
|
||||
Error(ErrorKind, String),
|
||||
Password {
|
||||
id: String,
|
||||
uuid: Arc<str>,
|
||||
username: String,
|
||||
username: Option<String>,
|
||||
password: SecureString,
|
||||
description: Option<String>,
|
||||
password_hidden: bool,
|
||||
tx: SecretSender,
|
||||
error: Option<(ErrorKind, String)>,
|
||||
},
|
||||
RemoveProfile(ConnectionId),
|
||||
|
|
@ -189,6 +184,7 @@ pub struct NmState {
|
|||
pub struct Page {
|
||||
entity: page::Entity,
|
||||
nm_task: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
secret_tx: Option<tokio::sync::mpsc::Sender<nm_secret_agent::Request>>,
|
||||
nm_state: Option<NmState>,
|
||||
dialog: Option<VpnDialog>,
|
||||
view_more_popup: Option<ConnectionId>,
|
||||
|
|
@ -242,10 +238,13 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
password,
|
||||
password_hidden,
|
||||
error,
|
||||
description,
|
||||
..
|
||||
} => {
|
||||
let username = widget::text_input(fl!("username"), username.as_str())
|
||||
.on_input(Message::UsernameUpdate);
|
||||
let username = username.as_ref().map(|username| {
|
||||
widget::text_input(fl!("username"), username.as_str())
|
||||
.on_input(Message::UsernameUpdate)
|
||||
});
|
||||
|
||||
let password = widget::text_input::secure_input(
|
||||
fl!("password"),
|
||||
|
|
@ -254,8 +253,14 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
*password_hidden,
|
||||
)
|
||||
.on_input(|input| Message::PasswordUpdate(SecureString::from(input)))
|
||||
.on_submit(|_| Message::ConnectWithPassword);
|
||||
let (err_kind, error) = if let Some(err) = error.as_ref() {
|
||||
.on_submit(|_| {
|
||||
if error.is_some() {
|
||||
Message::RetryWithPassword
|
||||
} else {
|
||||
Message::ConnectWithPassword
|
||||
}
|
||||
});
|
||||
let (err_kind, error_text) = if let Some(err) = error.as_ref() {
|
||||
(
|
||||
Some(err.0),
|
||||
Some(widget::text::body(err.1.as_str()).wrapping(Wrapping::Word)),
|
||||
|
|
@ -266,13 +271,17 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
|
||||
let controls = widget::column::with_capacity(2)
|
||||
.spacing(12)
|
||||
.push(username)
|
||||
.push_maybe(username)
|
||||
.push(password)
|
||||
.push_maybe(error)
|
||||
.push_maybe(error_text)
|
||||
.apply(Element::from);
|
||||
|
||||
let primary_action = widget::button::suggested(fl!("connect"))
|
||||
.on_press(Message::ConnectWithPassword);
|
||||
let primary_action =
|
||||
widget::button::suggested(fl!("connect")).on_press(if error.is_some() {
|
||||
Message::RetryWithPassword
|
||||
} else {
|
||||
Message::ConnectWithPassword
|
||||
});
|
||||
|
||||
let secondary_action =
|
||||
widget::button::standard(fl!("cancel")).on_press(Message::CancelDialog);
|
||||
|
|
@ -284,7 +293,11 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
fl!("auth-dialog")
|
||||
})
|
||||
.icon(icon::from_name("network-vpn-symbolic").size(64))
|
||||
.body(fl!("auth-dialog", "vpn-description"))
|
||||
.body(if let Some(description) = description.as_ref() {
|
||||
description.clone()
|
||||
} else {
|
||||
fl!("auth-dialog", "vpn-description")
|
||||
})
|
||||
.control(controls)
|
||||
.primary_action(primary_action)
|
||||
.secondary_action(secondary_action)
|
||||
|
|
@ -347,8 +360,10 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
}
|
||||
|
||||
fn on_enter(&mut self) -> cosmic::Task<crate::pages::Message> {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(4);
|
||||
self.secret_tx = Some(tx);
|
||||
if self.nm_task.is_none() {
|
||||
return cosmic::task::future(async move {
|
||||
return cosmic::Task::batch([cosmic::task::future(async move {
|
||||
zbus::Connection::system()
|
||||
.await
|
||||
.context("failed to create system dbus connection")
|
||||
|
|
@ -356,10 +371,15 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
|why| Message::Error(ErrorKind::DbusConnection, why.to_string()),
|
||||
Message::NetworkManagerConnect,
|
||||
)
|
||||
});
|
||||
}),
|
||||
cosmic::Task::stream(
|
||||
cosmic_settings_network_manager_subscription::nm_secret_agent::secret_agent_stream("com.system76.CosmicSettings.VPN.NetworkManager.SecretAgent", rx),
|
||||
)
|
||||
.map(|m| crate::pages::Message::Vpn(Message::SecretAgent(m))),
|
||||
]);
|
||||
}
|
||||
|
||||
Task::none()
|
||||
cosmic::Task::none()
|
||||
}
|
||||
|
||||
fn on_leave(&mut self) -> Task<crate::pages::Message> {
|
||||
|
|
@ -401,19 +421,15 @@ impl Page {
|
|||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Message::KnownConnections(connections) => {
|
||||
self.known_connections = connections;
|
||||
}
|
||||
|
||||
Message::UpdateDevices(devices) => {
|
||||
self.update_devices(devices);
|
||||
}
|
||||
|
||||
Message::UpdateState(state) => {
|
||||
self.update_active_conns(state);
|
||||
}
|
||||
|
||||
Message::NetworkManager(
|
||||
network_manager::Event::ActiveConns | network_manager::Event::Devices,
|
||||
) => {
|
||||
|
|
@ -425,7 +441,6 @@ impl Page {
|
|||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Message::NetworkManager(network_manager::Event::Init {
|
||||
conn,
|
||||
sender,
|
||||
|
|
@ -447,21 +462,16 @@ impl Page {
|
|||
update_devices(conn),
|
||||
]);
|
||||
}
|
||||
|
||||
Message::NetworkManager(_event) => (),
|
||||
|
||||
Message::AddNetwork => return add_network(),
|
||||
|
||||
Message::AddWireGuardDevice(device, filename, path) => {
|
||||
self.dialog = Some(VpnDialog::WireGuardName(device, filename, path));
|
||||
}
|
||||
|
||||
Message::WireGuardDeviceInput(input) => {
|
||||
if let Some(VpnDialog::WireGuardName(ref mut device, ..)) = self.dialog {
|
||||
*device = input
|
||||
}
|
||||
}
|
||||
|
||||
Message::WireGuardConfig => {
|
||||
if let Some(VpnDialog::WireGuardName(device, filename, path)) = self.dialog.take() {
|
||||
return cosmic::task::future(async move {
|
||||
|
|
@ -474,7 +484,6 @@ impl Page {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
Message::Activate(uuid) => {
|
||||
self.close_popup_and_apply_updates();
|
||||
|
||||
|
|
@ -502,13 +511,14 @@ impl Page {
|
|||
self.dialog = Some(VpnDialog::Password {
|
||||
id: settings.id.clone(),
|
||||
uuid: uuid.clone(),
|
||||
username: settings.username.clone().unwrap_or_default(),
|
||||
username: settings.username.clone(),
|
||||
password: SecureString::from(""),
|
||||
description: None,
|
||||
password_hidden: true,
|
||||
error: None,
|
||||
tx: Arc::new(Mutex::new(None)),
|
||||
});
|
||||
}
|
||||
|
||||
_ => {
|
||||
let connection_name = settings.id.clone();
|
||||
let username = settings.username.clone();
|
||||
|
|
@ -521,9 +531,12 @@ impl Page {
|
|||
)),
|
||||
id: connection_name.clone(),
|
||||
uuid,
|
||||
username: username.clone().unwrap_or_default(),
|
||||
username: username.clone(),
|
||||
description: None,
|
||||
password: SecureString::from(""),
|
||||
password_hidden: true,
|
||||
// TODO grab from the current dialog
|
||||
tx: Arc::new(Mutex::new(None)),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -533,19 +546,16 @@ impl Page {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Message::Deactivate(uuid) => {
|
||||
self.close_popup_and_apply_updates();
|
||||
if let Some(NmState { ref sender, .. }) = self.nm_state {
|
||||
_ = sender.unbounded_send(network_manager::Request::Deactivate(uuid));
|
||||
}
|
||||
}
|
||||
|
||||
Message::RemoveProfileRequest(uuid) => {
|
||||
self.view_more_popup = None;
|
||||
self.dialog = Some(VpnDialog::RemoveProfile(uuid));
|
||||
}
|
||||
|
||||
Message::RemoveProfile(uuid) => {
|
||||
self.dialog = None;
|
||||
self.close_popup_and_apply_updates();
|
||||
|
|
@ -553,7 +563,6 @@ impl Page {
|
|||
_ = sender.unbounded_send(network_manager::Request::Remove(uuid));
|
||||
}
|
||||
}
|
||||
|
||||
Message::ViewMore(uuid) => {
|
||||
self.view_more_popup = uuid;
|
||||
if self.view_more_popup.is_none() {
|
||||
|
|
@ -576,7 +585,6 @@ impl Page {
|
|||
.await
|
||||
});
|
||||
}
|
||||
|
||||
Message::Refresh => {
|
||||
if let Some(NmState { ref conn, .. }) = self.nm_state {
|
||||
return cosmic::Task::batch(vec![
|
||||
|
|
@ -586,7 +594,6 @@ impl Page {
|
|||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Message::PasswordUpdate(pass) => {
|
||||
if let Some(VpnDialog::Password {
|
||||
ref mut password, ..
|
||||
|
|
@ -595,12 +602,105 @@ impl Page {
|
|||
*password = pass;
|
||||
}
|
||||
}
|
||||
|
||||
Message::ConnectWithPassword => {
|
||||
let Some(dialog) = self.dialog.take() else {
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
if let VpnDialog::Password {
|
||||
id,
|
||||
uuid,
|
||||
username,
|
||||
password,
|
||||
tx,
|
||||
..
|
||||
} = dialog
|
||||
{
|
||||
let username_unwrapped = username.clone().unwrap_or_default();
|
||||
let task = self.activate_with_password(
|
||||
id.clone(),
|
||||
uuid.clone(),
|
||||
username_unwrapped.clone(),
|
||||
password.clone(),
|
||||
);
|
||||
let sec_tx = self.secret_tx.clone();
|
||||
return task
|
||||
.then(move |_| {
|
||||
let sec_tx = sec_tx.clone();
|
||||
let uuid = uuid.clone();
|
||||
let username = username.clone();
|
||||
let password = password.clone();
|
||||
let tx = tx.clone();
|
||||
let id = id.clone();
|
||||
Task::future(async move {
|
||||
let mut guard = tx.lock().await;
|
||||
if let Some(sender) = guard.take() {
|
||||
let _ = sender.send(password);
|
||||
} else {
|
||||
// apply password and username then
|
||||
if let Some(sec_tx) = sec_tx {
|
||||
let (applied_tx, applied_rx) =
|
||||
tokio::sync::oneshot::channel();
|
||||
if let Err(err) = sec_tx
|
||||
.send(nm_secret_agent::Request::SetSecrets {
|
||||
setting_name: "vpn".to_string(),
|
||||
uuid: uuid.to_string(),
|
||||
secrets: HashMap::from_iter([
|
||||
// username and password
|
||||
(
|
||||
"username".to_string(),
|
||||
username.clone().unwrap_or_default().into(),
|
||||
),
|
||||
(
|
||||
"password".to_string(),
|
||||
password.clone().into(),
|
||||
),
|
||||
]),
|
||||
applied_tx,
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::error!(%err, "failed to apply secret");
|
||||
}
|
||||
// wait max 1s for the applied signal
|
||||
if let Err(err) = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(1),
|
||||
applied_rx,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(%err, "failed to apply secret");
|
||||
}
|
||||
}
|
||||
// activate
|
||||
if let Err(why) = nmcli::connect(&id).await {
|
||||
return Message::VpnDialogError(VpnDialog::Password {
|
||||
error: Some((
|
||||
ErrorKind::Connect,
|
||||
format!("failed to connect to VPN: {why}"),
|
||||
)),
|
||||
id: id.clone(),
|
||||
uuid,
|
||||
username: username.clone(),
|
||||
description: None,
|
||||
password,
|
||||
password_hidden: true,
|
||||
tx: Arc::new(Mutex::new(None)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Message::Refresh
|
||||
})
|
||||
})
|
||||
.map(crate::app::Message::from);
|
||||
}
|
||||
}
|
||||
Message::RetryWithPassword => {
|
||||
let Some(dialog) = self.dialog.take() else {
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
if let VpnDialog::Password {
|
||||
id,
|
||||
uuid,
|
||||
|
|
@ -609,18 +709,54 @@ impl Page {
|
|||
..
|
||||
} = dialog
|
||||
{
|
||||
return self
|
||||
.activate_with_password(id, uuid, username, password)
|
||||
let username_unwrapped = username.unwrap_or_default();
|
||||
let sec_tx = self.secret_tx.clone();
|
||||
let task = self.activate_with_password(
|
||||
id.clone(),
|
||||
uuid.clone(),
|
||||
username_unwrapped.clone(),
|
||||
password.clone(),
|
||||
);
|
||||
return task
|
||||
.then(move |_| {
|
||||
let sec_tx = sec_tx.clone();
|
||||
let uuid = uuid.clone();
|
||||
let username = username_unwrapped.clone();
|
||||
let password = password.clone();
|
||||
Task::future(async move {
|
||||
if let Some(sec_tx) = sec_tx {
|
||||
let (applied_tx, applied_rx) = tokio::sync::oneshot::channel();
|
||||
let _ = sec_tx
|
||||
.send(nm_secret_agent::Request::SetSecrets {
|
||||
setting_name: "vpn".to_string(),
|
||||
uuid: uuid.to_string(),
|
||||
secrets: HashMap::from_iter([
|
||||
// username and password
|
||||
("username".to_string(), username.clone().into()),
|
||||
("password".to_string(), password.clone().into()),
|
||||
]),
|
||||
applied_tx,
|
||||
})
|
||||
.await;
|
||||
// wait max 1s for the applied signal
|
||||
let _ = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(1),
|
||||
applied_rx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Message::Activate(uuid)
|
||||
})
|
||||
})
|
||||
.map(crate::app::Message::from);
|
||||
}
|
||||
}
|
||||
|
||||
Message::UsernameUpdate(user) => {
|
||||
if let Some(VpnDialog::Password {
|
||||
ref mut username, ..
|
||||
}) = self.dialog
|
||||
{
|
||||
*username = user;
|
||||
*username = Some(user);
|
||||
}
|
||||
}
|
||||
Message::CancelDialog => {
|
||||
|
|
@ -635,19 +771,65 @@ impl Page {
|
|||
*password_hidden = !*password_hidden;
|
||||
}
|
||||
}
|
||||
|
||||
Message::Error(error_kind, why) => {
|
||||
tracing::error!(?error_kind, why);
|
||||
self.dialog = Some(VpnDialog::Error(error_kind, why))
|
||||
}
|
||||
|
||||
Message::NetworkManagerConnect(conn) => {
|
||||
return self.connect(conn.clone());
|
||||
}
|
||||
|
||||
Message::VpnDialogError(vpn_dialog) => {
|
||||
self.dialog = Some(vpn_dialog);
|
||||
}
|
||||
Message::SecretAgent(e) => match e {
|
||||
nm_secret_agent::Event::RequestSecret {
|
||||
uuid: _,
|
||||
name,
|
||||
previous,
|
||||
description,
|
||||
tx,
|
||||
} => {
|
||||
self.dialog = Some(VpnDialog::Password {
|
||||
id: name.clone(),
|
||||
uuid: Arc::from(""),
|
||||
username: None,
|
||||
description: description,
|
||||
password: previous,
|
||||
password_hidden: true,
|
||||
tx,
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
nm_secret_agent::Event::CancelGetSecrets { uuid: _, name: _ } => {
|
||||
self.dialog = self
|
||||
.dialog
|
||||
.take()
|
||||
.filter(|d| !matches!(d, &VpnDialog::Password { .. }));
|
||||
}
|
||||
nm_secret_agent::Event::Failed(error) => {
|
||||
tracing::error!(%error, "secret agent failure");
|
||||
if let Some(VpnDialog::Password {
|
||||
id,
|
||||
uuid,
|
||||
username,
|
||||
password,
|
||||
description,
|
||||
..
|
||||
}) = self.dialog.take()
|
||||
{
|
||||
self.dialog = Some(VpnDialog::Password {
|
||||
error: Some((ErrorKind::DbusConnection, error.to_string())),
|
||||
id,
|
||||
uuid,
|
||||
username,
|
||||
description,
|
||||
password,
|
||||
password_hidden: true,
|
||||
tx: Arc::new(Mutex::new(None)),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Task::none()
|
||||
|
|
@ -666,20 +848,11 @@ impl Page {
|
|||
error: Some((ErrorKind::WithPassword("username"), why.to_string())),
|
||||
id: connection_name.clone(),
|
||||
uuid,
|
||||
username,
|
||||
password,
|
||||
password_hidden: true,
|
||||
});
|
||||
}
|
||||
|
||||
if let Err(why) = nmcli::set_password_flags_none(&connection_name).await {
|
||||
return Message::VpnDialogError(VpnDialog::Password {
|
||||
error: Some((ErrorKind::WithPassword("password-flags"), why.to_string())),
|
||||
id: connection_name.clone(),
|
||||
uuid,
|
||||
username,
|
||||
username: Some(username),
|
||||
description: None,
|
||||
password,
|
||||
password_hidden: true,
|
||||
tx: Arc::new(Mutex::new(None)),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -688,42 +861,11 @@ impl Page {
|
|||
error: Some((ErrorKind::Config, why.to_string())),
|
||||
id: connection_name.clone(),
|
||||
uuid,
|
||||
username,
|
||||
password,
|
||||
password_hidden: true,
|
||||
});
|
||||
}
|
||||
|
||||
if let Err(why) = nmcli::set_password_flags_none(&connection_name).await {
|
||||
return Message::VpnDialogError(VpnDialog::Password {
|
||||
error: Some((ErrorKind::WithPassword("password-flags"), why.to_string())),
|
||||
id: connection_name.clone(),
|
||||
uuid,
|
||||
username,
|
||||
password,
|
||||
password_hidden: true,
|
||||
});
|
||||
}
|
||||
|
||||
if let Err(why) = nmcli::set_password(&connection_name, password.unsecure()).await {
|
||||
return Message::VpnDialogError(VpnDialog::Password {
|
||||
error: Some((ErrorKind::WithPassword("password"), why.to_string())),
|
||||
id: connection_name.clone(),
|
||||
uuid,
|
||||
username,
|
||||
password,
|
||||
password_hidden: true,
|
||||
});
|
||||
}
|
||||
|
||||
if let Err(why) = nmcli::connect(&connection_name).await {
|
||||
return Message::VpnDialogError(VpnDialog::Password {
|
||||
error: Some((ErrorKind::Connect, why.to_string())),
|
||||
id: connection_name.clone(),
|
||||
uuid,
|
||||
username,
|
||||
username: Some(username),
|
||||
password,
|
||||
description: None,
|
||||
password_hidden: true,
|
||||
tx: Arc::new(Mutex::new(None)),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1065,25 +1207,24 @@ fn connection_settings(conn: zbus::Connection) -> Task<crate::app::Message> {
|
|||
let id = connection.get("id")?.downcast_ref::<String>().ok()?;
|
||||
let uuid = connection.get("uuid")?.downcast_ref::<String>().ok()?;
|
||||
|
||||
let (username, connection_type, password_flag) = vpn
|
||||
let (connection_type, username, password_flag) = vpn
|
||||
.get("data")
|
||||
.and_then(|data| data.downcast_ref::<zbus::zvariant::Dict>().ok())
|
||||
.map(|dict| {
|
||||
let (mut connection_type, mut password_flag) = (None, None);
|
||||
|
||||
let username = dict
|
||||
.get::<String, String>(&String::from("username"))
|
||||
.ok()
|
||||
.flatten()
|
||||
.filter(|value| !value.is_empty());
|
||||
|
||||
if let Some("password") = dict
|
||||
let mut username = vpn
|
||||
.get("user-name")
|
||||
.and_then(|u| u.downcast_ref::<String>().ok());
|
||||
if dict
|
||||
.get::<String, String>(&String::from("connection-type"))
|
||||
.ok()
|
||||
.flatten()
|
||||
.as_deref()
|
||||
// may be "password" or "password-tls"
|
||||
.is_some_and(|p| p.starts_with("password"))
|
||||
{
|
||||
connection_type = Some(ConnectionType::Password);
|
||||
username = Some(username.unwrap_or_default());
|
||||
|
||||
password_flag = dict
|
||||
.get::<String, String>(&String::from("password-flags"))
|
||||
|
|
@ -1098,7 +1239,7 @@ fn connection_settings(conn: zbus::Connection) -> Task<crate::app::Message> {
|
|||
});
|
||||
}
|
||||
|
||||
(username, connection_type, password_flag)
|
||||
(connection_type, username, password_flag)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
|
|
|
|||
|
|
@ -13,21 +13,6 @@ pub async fn set_username(connection_name: &str, username: &str) -> Result<(), S
|
|||
.apply(crate::utils::map_stderr_output)
|
||||
}
|
||||
|
||||
pub async fn set_password_flags_none(connection_name: &str) -> Result<(), String> {
|
||||
tokio::process::Command::new("nmcli")
|
||||
.args([
|
||||
"con",
|
||||
"mod",
|
||||
connection_name,
|
||||
"+vpn.data",
|
||||
"password-flags=0",
|
||||
])
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await
|
||||
.apply(crate::utils::map_stderr_output)
|
||||
}
|
||||
|
||||
pub async fn add_fallback(connection_name: &str) -> Result<(), String> {
|
||||
tokio::process::Command::new("nmcli")
|
||||
.args([
|
||||
|
|
@ -43,21 +28,6 @@ pub async fn add_fallback(connection_name: &str) -> Result<(), String> {
|
|||
.apply(crate::utils::map_stderr_output)
|
||||
}
|
||||
|
||||
pub async fn set_password(connection_name: &str, password: &str) -> Result<(), String> {
|
||||
tokio::process::Command::new("nmcli")
|
||||
.args([
|
||||
"con",
|
||||
"mod",
|
||||
connection_name,
|
||||
"vpn.secrets",
|
||||
&format!("password={password}"),
|
||||
])
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await
|
||||
.apply(crate::utils::map_stderr_output)
|
||||
}
|
||||
|
||||
pub async fn connect(connection_name: &str) -> Result<(), String> {
|
||||
tokio::process::Command::new("nmcli")
|
||||
.args(["con", "up", connection_name])
|
||||
|
|
|
|||
|
|
@ -20,10 +20,14 @@ use cosmic_settings_network_manager_subscription::{
|
|||
available_wifi::{AccessPoint, NetworkType},
|
||||
current_networks::ActiveConnectionInfo,
|
||||
hw_address::HwAddress,
|
||||
nm_secret_agent,
|
||||
};
|
||||
use cosmic_settings_page::{self as page, Section, section};
|
||||
use futures::StreamExt;
|
||||
use secure_string::SecureString;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::pages::networking::SecretSender;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
|
|
@ -49,6 +53,8 @@ pub enum Message {
|
|||
Forget(network_manager::SSID),
|
||||
/// An update from the network manager daemon
|
||||
NetworkManager(network_manager::Event),
|
||||
/// An update from the secret agent
|
||||
SecretAgent(network_manager::nm_secret_agent::Event),
|
||||
/// Successfully connected to the system dbus.
|
||||
NetworkManagerConnect(zbus::Connection),
|
||||
/// Request an auth dialog
|
||||
|
|
@ -87,7 +93,7 @@ impl From<Message> for crate::pages::Message {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug)]
|
||||
enum WiFiDialog {
|
||||
Forget(network_manager::SSID),
|
||||
Password {
|
||||
|
|
@ -96,6 +102,7 @@ enum WiFiDialog {
|
|||
identity: Option<String>,
|
||||
password: SecureString,
|
||||
password_hidden: bool,
|
||||
tx: SecretSender,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -103,7 +110,7 @@ enum WiFiDialog {
|
|||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct QRCodeDrawer {
|
||||
ssid: network_manager::SSID,
|
||||
password: Option<String>,
|
||||
password: Option<SecureString>,
|
||||
security_type: NetworkType,
|
||||
}
|
||||
|
||||
|
|
@ -111,6 +118,7 @@ struct QRCodeDrawer {
|
|||
pub struct Page {
|
||||
entity: page::Entity,
|
||||
nm_task: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
secret_tx: Option<tokio::sync::mpsc::Sender<nm_secret_agent::Request>>,
|
||||
nm_state: Option<NmState>,
|
||||
/// When defined, displays connections for the specific device.
|
||||
active_device: Option<Arc<network_manager::devices::DeviceInfo>>,
|
||||
|
|
@ -245,7 +253,7 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
));
|
||||
|
||||
if let Some(ref pass) = drawer.password {
|
||||
info_items = info_items.add(widget::settings::item(fl!("password"), pass.as_str()));
|
||||
info_items = info_items.add(widget::settings::item(fl!("password"), pass.unsecure()));
|
||||
}
|
||||
|
||||
let content = column::column()
|
||||
|
|
@ -279,8 +287,10 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
}
|
||||
|
||||
fn on_enter(&mut self) -> cosmic::Task<crate::pages::Message> {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(4);
|
||||
self.secret_tx = Some(tx);
|
||||
if self.nm_task.is_none() {
|
||||
return cosmic::Task::future(async move {
|
||||
return Task::batch(vec![cosmic::Task::future(async move {
|
||||
zbus::Connection::system()
|
||||
.await
|
||||
.context("failed to create system dbus connection")
|
||||
|
|
@ -289,7 +299,10 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
Message::NetworkManagerConnect,
|
||||
)
|
||||
.apply(crate::pages::Message::WiFi)
|
||||
});
|
||||
}), cosmic::Task::stream(
|
||||
cosmic_settings_network_manager_subscription::nm_secret_agent::secret_agent_stream("com.system76.CosmicSettings.WiFi.NetworkManager.SecretAgent", rx),
|
||||
)
|
||||
.map(|m| crate::pages::Message::WiFi(Message::SecretAgent(m)))]);
|
||||
}
|
||||
|
||||
Task::none()
|
||||
|
|
@ -350,16 +363,30 @@ impl Page {
|
|||
hw_address,
|
||||
password: SecureString::from(""),
|
||||
password_hidden: true,
|
||||
tx: Arc::new(Mutex::new(None)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
network_manager::Request::SelectAccessPoint(
|
||||
ssid,
|
||||
_hw_address,
|
||||
_network_type,
|
||||
hw_address,
|
||||
network_type,
|
||||
_tx,
|
||||
) => {
|
||||
self.connecting.remove(ssid.as_ref());
|
||||
if success || matches!(network_type, NetworkType::Open) {
|
||||
self.connecting.remove(ssid.as_ref());
|
||||
} else {
|
||||
self.dialog = Some(WiFiDialog::Password {
|
||||
ssid: ssid.into(),
|
||||
identity: matches!(network_type, NetworkType::EAP)
|
||||
.then(String::new),
|
||||
hw_address,
|
||||
password: SecureString::from(""),
|
||||
password_hidden: true,
|
||||
tx: Arc::new(Mutex::new(None)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_ => (),
|
||||
|
|
@ -371,11 +398,9 @@ impl Page {
|
|||
return update_devices(conn.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Message::UpdateDevices(devices) => {
|
||||
self.update_devices(devices);
|
||||
}
|
||||
|
||||
Message::UpdateState(state) => {
|
||||
self.update_state(state);
|
||||
|
||||
|
|
@ -383,7 +408,6 @@ impl Page {
|
|||
return connection_settings(conn.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Message::NetworkManager(
|
||||
network_manager::Event::ActiveConns
|
||||
| network_manager::Event::Devices
|
||||
|
|
@ -397,11 +421,9 @@ impl Page {
|
|||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Message::ConnectionSettings(settings) => {
|
||||
self.ssid_to_uuid = settings;
|
||||
}
|
||||
|
||||
Message::NetworkManager(network_manager::Event::Init {
|
||||
conn,
|
||||
sender,
|
||||
|
|
@ -416,7 +438,6 @@ impl Page {
|
|||
|
||||
return update_devices(conn);
|
||||
}
|
||||
|
||||
Message::NetworkManager(network_manager::Event::WiFiCredentials {
|
||||
ssid,
|
||||
password,
|
||||
|
|
@ -431,7 +452,7 @@ impl Page {
|
|||
NetworkType::EAP => "WPA",
|
||||
NetworkType::Open => "",
|
||||
};
|
||||
let escaped_password = escape_wifi_qr_string(pass);
|
||||
let escaped_password = escape_wifi_qr_string(pass.unsecure());
|
||||
format!(
|
||||
"WIFI:T:{};S:{};P:{};;",
|
||||
security, escaped_ssid, escaped_password
|
||||
|
|
@ -451,11 +472,9 @@ impl Page {
|
|||
// Open the context drawer
|
||||
return cosmic::task::message(crate::app::Message::OpenContextDrawer(self.entity));
|
||||
}
|
||||
|
||||
Message::AddNetwork => {
|
||||
tokio::task::spawn(super::nm_add_wifi());
|
||||
}
|
||||
|
||||
Message::Connect(ssid) => {
|
||||
if let Some(nm) = self.nm_state.as_mut() {
|
||||
let Some(ap) = nm
|
||||
|
|
@ -474,10 +493,10 @@ impl Page {
|
|||
ssid,
|
||||
ap.hw_address,
|
||||
ap.network_type,
|
||||
self.secret_tx.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Message::IdentityUpdate(new_identity) => {
|
||||
if let Some(WiFiDialog::Password {
|
||||
ref mut identity, ..
|
||||
|
|
@ -486,7 +505,6 @@ impl Page {
|
|||
*identity = Some(new_identity);
|
||||
}
|
||||
}
|
||||
|
||||
Message::PasswordRequest(ssid) => {
|
||||
if let Some(nm) = self.nm_state.as_mut() {
|
||||
let Some(ap) = nm
|
||||
|
|
@ -498,16 +516,17 @@ impl Page {
|
|||
else {
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
self.dialog = Some(WiFiDialog::Password {
|
||||
ssid,
|
||||
identity: matches!(ap.network_type, NetworkType::EAP).then(String::new),
|
||||
hw_address: ap.hw_address,
|
||||
password: SecureString::from(""),
|
||||
password_hidden: true,
|
||||
tx: Arc::new(Mutex::new(None)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Message::PasswordUpdate(pass) => {
|
||||
if let Some(WiFiDialog::Password {
|
||||
ref mut password, ..
|
||||
|
|
@ -516,7 +535,6 @@ impl Page {
|
|||
*password = pass;
|
||||
}
|
||||
}
|
||||
|
||||
Message::ConnectWithPassword => {
|
||||
let Some(dialog) = self.dialog.take() else {
|
||||
return Task::none();
|
||||
|
|
@ -527,22 +545,31 @@ impl Page {
|
|||
identity,
|
||||
password,
|
||||
hw_address,
|
||||
tx,
|
||||
..
|
||||
} = dialog
|
||||
&& let Some(nm) = self.nm_state.as_mut()
|
||||
{
|
||||
self.connecting.insert(ssid.clone());
|
||||
_ = nm
|
||||
.sender
|
||||
.unbounded_send(network_manager::Request::Authenticate {
|
||||
ssid: ssid.to_string(),
|
||||
identity,
|
||||
hw_address,
|
||||
password,
|
||||
});
|
||||
let nm_sender = nm.sender.clone();
|
||||
let secret_tx = self.secret_tx.clone();
|
||||
return Task::future(async move {
|
||||
let mut guard = tx.lock().await;
|
||||
if let Some(tx) = guard.take() {
|
||||
_ = tx.send(password);
|
||||
} else {
|
||||
_ = nm_sender.unbounded_send(network_manager::Request::Authenticate {
|
||||
ssid: ssid.to_string(),
|
||||
identity,
|
||||
hw_address,
|
||||
password,
|
||||
secret_tx,
|
||||
});
|
||||
}
|
||||
})
|
||||
.discard();
|
||||
}
|
||||
}
|
||||
|
||||
Message::TogglePasswordVisibility => {
|
||||
if let Some(WiFiDialog::Password {
|
||||
ref mut password_hidden,
|
||||
|
|
@ -552,23 +579,39 @@ impl Page {
|
|||
*password_hidden = !*password_hidden;
|
||||
}
|
||||
}
|
||||
|
||||
Message::QRCodeRequest(ssid) => {
|
||||
self.view_more_popup = None;
|
||||
if let Some(nm) = self.nm_state.as_mut() {
|
||||
let Some(ap): Option<&AccessPoint> = self.nm_state.as_ref().and_then(|nm| {
|
||||
nm.state
|
||||
.wireless_access_points
|
||||
.iter()
|
||||
.chain(nm.state.known_access_points.iter())
|
||||
.find(|ap| ap.ssid == ssid)
|
||||
}) else {
|
||||
return Task::none();
|
||||
};
|
||||
if let Some(nm) = self.nm_state.as_ref() {
|
||||
let uuid = self
|
||||
.ssid_to_uuid
|
||||
.get(ssid.as_ref())
|
||||
.map(|uuid| uuid.as_ref())
|
||||
.unwrap_or_default();
|
||||
_ = nm
|
||||
.sender
|
||||
.unbounded_send(network_manager::Request::GetWiFiCredentials(ssid));
|
||||
.unbounded_send(network_manager::Request::GetWiFiCredentials(
|
||||
ssid,
|
||||
uuid.into(),
|
||||
ap.network_type,
|
||||
self.secret_tx.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Message::ViewMore(ssid) => {
|
||||
self.view_more_popup = ssid;
|
||||
if self.view_more_popup.is_none() {
|
||||
self.close_popup_and_apply_updates();
|
||||
}
|
||||
}
|
||||
|
||||
Message::Disconnect(ssid) => {
|
||||
self.close_popup_and_apply_updates();
|
||||
if let Some(nm) = self.nm_state.as_mut() {
|
||||
|
|
@ -577,12 +620,10 @@ impl Page {
|
|||
.unbounded_send(network_manager::Request::Disconnect(ssid));
|
||||
}
|
||||
}
|
||||
|
||||
Message::ForgetRequest(ssid) => {
|
||||
self.dialog = Some(WiFiDialog::Forget(ssid));
|
||||
self.view_more_popup = None;
|
||||
}
|
||||
|
||||
Message::Forget(ssid) => {
|
||||
self.dialog = None;
|
||||
self.close_popup_and_apply_updates();
|
||||
|
|
@ -592,7 +633,6 @@ impl Page {
|
|||
.unbounded_send(network_manager::Request::Forget(ssid));
|
||||
}
|
||||
}
|
||||
|
||||
Message::Settings(ssid) => {
|
||||
self.close_popup_and_apply_updates();
|
||||
|
||||
|
|
@ -602,13 +642,11 @@ impl Page {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
Message::SubmitIdentity => {
|
||||
if self.dialog.is_some() {
|
||||
return focus_next();
|
||||
}
|
||||
}
|
||||
|
||||
Message::WiFiEnable(enable) => {
|
||||
if let Some(nm) = self.nm_state.as_mut() {
|
||||
_ = nm
|
||||
|
|
@ -617,26 +655,92 @@ impl Page {
|
|||
_ = nm.sender.unbounded_send(network_manager::Request::Reload);
|
||||
}
|
||||
}
|
||||
|
||||
Message::CancelDialog => {
|
||||
self.dialog = None;
|
||||
}
|
||||
|
||||
Message::Error(why) => {
|
||||
tracing::error!(why);
|
||||
}
|
||||
|
||||
Message::SelectDevice(device) => {
|
||||
// TODO: Per-device wifi connection handling.
|
||||
self.active_device = Some(device);
|
||||
}
|
||||
|
||||
Message::NetworkManagerConnect(conn) => {
|
||||
return cosmic::task::batch(vec![
|
||||
self.connect(conn.clone()),
|
||||
connection_settings(conn),
|
||||
]);
|
||||
}
|
||||
Message::SecretAgent(event) => match event {
|
||||
nm_secret_agent::Event::RequestSecret {
|
||||
uuid,
|
||||
name,
|
||||
description: _, // TODO do we want to display the description?
|
||||
previous,
|
||||
tx,
|
||||
} => {
|
||||
let ssid = self
|
||||
.ssid_to_uuid
|
||||
.iter()
|
||||
.find_map(|(ssid, conn_uuid)| {
|
||||
if conn_uuid.as_ref() == name.as_str() {
|
||||
Some(network_manager::SSID::from(ssid.as_ref()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let Some(ap): Option<&AccessPoint> = self.nm_state.as_ref().and_then(|nm| {
|
||||
nm.state
|
||||
.wireless_access_points
|
||||
.iter()
|
||||
.chain(nm.state.known_access_points.iter())
|
||||
.find(|ap| ap.ssid == ssid)
|
||||
}) else {
|
||||
tracing::error!(
|
||||
%uuid,
|
||||
%name,
|
||||
"received secret request for unknown connection"
|
||||
);
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
self.dialog = Some(WiFiDialog::Password {
|
||||
ssid,
|
||||
password: previous,
|
||||
password_hidden: true,
|
||||
hw_address: ap.hw_address,
|
||||
identity: matches!(ap.network_type, NetworkType::EAP).then(String::new),
|
||||
tx,
|
||||
});
|
||||
}
|
||||
nm_secret_agent::Event::CancelGetSecrets { uuid: _, name: _ } => {
|
||||
self.dialog = self
|
||||
.dialog
|
||||
.take()
|
||||
.filter(|d| !matches!(d, &WiFiDialog::Password { .. }));
|
||||
}
|
||||
nm_secret_agent::Event::Failed(error) => {
|
||||
tracing::error!(%error, "secret agent failure");
|
||||
if let Some(WiFiDialog::Password {
|
||||
ssid,
|
||||
password,
|
||||
identity,
|
||||
hw_address,
|
||||
..
|
||||
}) = self.dialog.take()
|
||||
{
|
||||
self.dialog = Some(WiFiDialog::Password {
|
||||
password,
|
||||
password_hidden: true,
|
||||
tx: Arc::new(Mutex::new(None)),
|
||||
ssid,
|
||||
identity,
|
||||
hw_address,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Task::none()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue