Skip to content

Commit 973128d

Browse files
committed
feat(vpn): add authentication dialog
1 parent 3b76f6d commit 973128d

File tree

3 files changed

+226
-13
lines changed

3 files changed

+226
-13
lines changed

cosmic-settings/Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,12 @@ upower_dbus = { git = "https://github.com/pop-os/dbus-settings-bindings" }
5656
url = "2.5.2"
5757
xkb-data = "0.2.1"
5858
zbus = { version = "4.4.0", features = ["tokio"] }
59+
secure-string = "0.3.0"
5960

6061
[dependencies.cosmic-settings-subscriptions]
61-
git = "https://github.com/pop-os/cosmic-settings-subscriptions"
62-
branch = "network"
63-
# path = "../../cosmic-settings-subscriptions"
62+
# git = "https://github.com/pop-os/cosmic-settings-subscriptions"
63+
# branch = "network"
64+
path = "../../cosmic-settings-subscriptions"
6465
features = ["network_manager", "pipewire", "pulse"]
6566

6667
[dependencies.icu]

cosmic-settings/src/pages/networking/vpn.rs

Lines changed: 212 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use cosmic_settings_subscriptions::network_manager::{
1717
};
1818
use futures::{FutureExt, StreamExt};
1919
use indexmap::IndexMap;
20+
use secure_string::SecureString;
2021
use slab::Slab;
2122
use zbus::zvariant::ObjectPath;
2223

@@ -29,6 +30,10 @@ pub enum Message {
2930
Activate(ConnectionId),
3031
/// Add a network connection
3132
AddNetwork,
33+
/// Cancels an active dialog.
34+
CancelDialog,
35+
/// Connect to a VPN with the given username and password
36+
ConnectWithPassword,
3237
/// Deactivate a connection.
3338
Deactivate(ConnectionId),
3439
/// An error occurred.
@@ -44,16 +49,22 @@ pub enum Message {
4449
tokio::sync::mpsc::Sender<crate::pages::Message>,
4550
),
4651
),
52+
/// Updates the password text input
53+
PasswordUpdate(String),
4754
/// Refresh devices and their connection profiles
4855
Refresh,
4956
/// Remove a connection profile
5057
RemoveProfile(ConnectionId),
5158
/// Opens settings page for the access point.
5259
Settings(ConnectionId),
60+
/// Toggles visibility of password input.
61+
TogglePasswordVisibility,
5362
/// Update NetworkManagerState
5463
UpdateState(NetworkManagerState),
5564
/// Update the devices lists
5665
UpdateDevices(Vec<network_manager::devices::DeviceInfo>),
66+
/// Updates the username text input
67+
UsernameUpdate(String),
5768
/// Display more options for an access point
5869
ViewMore(Option<ConnectionId>),
5970
}
@@ -74,7 +85,56 @@ impl From<Message> for crate::pages::Message {
7485
struct VpnConnectionSettings {
7586
path: ObjectPath<'static>,
7687
id: String,
77-
connection_type: String,
88+
username: Option<String>,
89+
connection_type: Option<ConnectionType>,
90+
password_flag: Option<PasswordFlag>,
91+
}
92+
93+
impl VpnConnectionSettings {
94+
fn password_required(&self) -> bool {
95+
self.connection_type.as_ref().map_or(false, |ct| match ct {
96+
ConnectionType::Password => true,
97+
ConnectionType::Unknown => false,
98+
}) && self
99+
.password_flag
100+
.as_ref()
101+
.map_or(false, |flag| match flag {
102+
PasswordFlag::NotRequired => false,
103+
_ => true,
104+
})
105+
}
106+
}
107+
108+
#[derive(Clone, Debug, Eq, PartialEq)]
109+
enum ConnectionType {
110+
Password,
111+
Unknown,
112+
}
113+
114+
#[derive(Clone, Debug, Eq, PartialEq)]
115+
enum PasswordFlag {
116+
/// The system is responsible for providing and storing this secret.
117+
None = 0,
118+
/// A user-session secret agent is responsible for providing and storing
119+
/// this secret; when it is required, agents will be asked to provide it.
120+
AgentOwned = 1,
121+
/// This secret should not be saved but should be requested from the user
122+
/// each time it is required. This flag should be used for One-Time-Pad
123+
/// secrets, PIN codes from hardware tokens, or if the user simply does not
124+
/// want to save the secret.
125+
NotSaved = 2,
126+
/// 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.
127+
NotRequired = 4,
128+
}
129+
130+
#[derive(Clone, Debug, Eq, PartialEq)]
131+
enum VpnDialog {
132+
Password {
133+
path: ObjectPath<'static>,
134+
username: String,
135+
password: SecureString,
136+
password_hidden: bool,
137+
},
78138
}
79139

80140
#[derive(Debug)]
@@ -89,6 +149,7 @@ pub struct NmState {
89149
pub struct Page {
90150
nm_task: Option<tokio::sync::oneshot::Sender<()>>,
91151
nm_state: Option<NmState>,
152+
dialog: Option<VpnDialog>,
92153
view_more_popup: Option<ConnectionId>,
93154
known_connections: IndexMap<UUID, VpnConnectionSettings>,
94155
/// Withhold device update if the view more popup is shown.
@@ -113,7 +174,50 @@ impl page::Page<crate::pages::Message> for Page {
113174
Some(vec![sections.insert(devices_view())])
114175
}
115176

116-
fn header_view(&self) -> Option<cosmic::Element<'_, crate::pages::Message>> {
177+
fn dialog(&self) -> Option<Element<crate::pages::Message>> {
178+
self.dialog.as_ref().map(|dialog| match dialog {
179+
VpnDialog::Password {
180+
path,
181+
username,
182+
password,
183+
password_hidden,
184+
} => {
185+
let username = widget::text_input(fl!("username"), username.as_str())
186+
.on_input(Message::UsernameUpdate);
187+
188+
let password = widget::text_input::secure_input(
189+
fl!("password"),
190+
password.unsecure(),
191+
Some(Message::TogglePasswordVisibility),
192+
*password_hidden,
193+
)
194+
.on_input(Message::PasswordUpdate)
195+
.on_submit(Message::ConnectWithPassword);
196+
197+
let controls = widget::column::with_capacity(2)
198+
.spacing(12)
199+
.push(username)
200+
.push(password)
201+
.apply(Element::from);
202+
203+
let primary_action = widget::button::suggested(fl!("connect"))
204+
.on_press(Message::ConnectWithPassword);
205+
206+
let secondary_action =
207+
widget::button::standard(fl!("cancel")).on_press(Message::CancelDialog);
208+
209+
widget::dialog(fl!("auth-dialog"))
210+
.body(fl!("auth-dialog", "vpn-description"))
211+
.control(controls)
212+
.primary_action(primary_action)
213+
.secondary_action(secondary_action)
214+
.apply(Element::from)
215+
.map(crate::pages::Message::Vpn)
216+
}
217+
})
218+
}
219+
220+
fn header_view(&self) -> Option<Element<'_, crate::pages::Message>> {
117221
Some(
118222
widget::button::standard(fl!("add-network"))
119223
.on_press(Message::AddNetwork)
@@ -150,6 +254,7 @@ impl page::Page<crate::pages::Message> for Page {
150254
self.nm_state = None;
151255
self.withheld_active_conns = None;
152256
self.withheld_devices = None;
257+
self.dialog = None;
153258

154259
if let Some(cancel) = self.nm_task.take() {
155260
_ = cancel.send(());
@@ -236,10 +341,19 @@ impl Page {
236341

237342
if let Some(NmState { ref sender, .. }) = self.nm_state {
238343
if let Some(settings) = self.known_connections.get(&uuid) {
239-
_ = sender.unbounded_send(network_manager::Request::Activate(
240-
ObjectPath::from_static_str_unchecked("/"),
241-
settings.path.clone(),
242-
));
344+
if settings.password_required() {
345+
self.dialog = Some(VpnDialog::Password {
346+
path: settings.path.clone(),
347+
username: settings.username.clone().unwrap_or_default(),
348+
password: SecureString::from(""),
349+
password_hidden: true,
350+
});
351+
} else {
352+
_ = sender.unbounded_send(network_manager::Request::Activate(
353+
ObjectPath::from_static_str_unchecked("/"),
354+
settings.path.clone(),
355+
));
356+
}
243357
}
244358
}
245359
}
@@ -290,6 +404,64 @@ impl Page {
290404
}
291405
}
292406

407+
Message::PasswordUpdate(pass) => {
408+
if let Some(VpnDialog::Password {
409+
ref mut password, ..
410+
}) = self.dialog
411+
{
412+
*password = SecureString::from(pass);
413+
}
414+
}
415+
416+
Message::ConnectWithPassword => {
417+
let Some(NmState { ref mut sender, .. }) = self.nm_state else {
418+
return Command::none();
419+
};
420+
421+
let Some(dialog) = self.dialog.take() else {
422+
return Command::none();
423+
};
424+
425+
match dialog {
426+
VpnDialog::Password {
427+
path,
428+
username,
429+
password,
430+
..
431+
} => {
432+
_ = sender.unbounded_send(network_manager::Request::ActivateWithPassword(
433+
ObjectPath::from_static_str_unchecked("/"),
434+
path,
435+
username,
436+
password,
437+
));
438+
}
439+
}
440+
}
441+
442+
Message::UsernameUpdate(user) => {
443+
if let Some(VpnDialog::Password {
444+
ref mut username, ..
445+
}) = self.dialog
446+
{
447+
*username = user;
448+
}
449+
}
450+
451+
Message::CancelDialog => {
452+
self.dialog = None;
453+
}
454+
455+
Message::TogglePasswordVisibility => {
456+
if let Some(VpnDialog::Password {
457+
ref mut password_hidden,
458+
..
459+
}) = self.dialog
460+
{
461+
*password_hidden = !*password_hidden;
462+
}
463+
}
464+
293465
Message::Error(why) => {
294466
tracing::error!(why, "error in VPN settings page");
295467
}
@@ -561,14 +733,42 @@ fn connection_settings(conn: zbus::Connection) -> Command<crate::app::Message> {
561733
let id = connection.get("id")?.downcast_ref::<String>().ok()?;
562734
let uuid = connection.get("uuid")?.downcast_ref::<String>().ok()?;
563735

564-
let connection_type = vpn
736+
let (username, connection_type, password_flag) = vpn
565737
.get("data")
566738
.and_then(|data| data.downcast_ref::<zbus::zvariant::Dict>().ok())
567-
.and_then(|dict| {
568-
dict.get::<String, String>(&String::from("connection-type"))
739+
.map(|dict| {
740+
let (mut username, mut connection_type, mut password_flag) =
741+
(None, None, None);
742+
743+
username = dict
744+
.get::<String, String>(&String::from("username"))
745+
.ok()
746+
.flatten()
747+
.filter(|value| !value.is_empty());
748+
749+
if let Some("password") = dict
750+
.get::<String, String>(&String::from("connection-type"))
569751
.ok()
752+
.flatten()
753+
.as_deref()
754+
{
755+
connection_type = Some(ConnectionType::Password);
756+
757+
password_flag = dict
758+
.get::<String, String>(&String::from("password-flags"))
759+
.ok()
760+
.flatten()
761+
.and_then(|value| match value.as_str() {
762+
"0" => Some(PasswordFlag::None),
763+
"1" => Some(PasswordFlag::AgentOwned),
764+
"2" => Some(PasswordFlag::NotSaved),
765+
"4" => Some(PasswordFlag::NotRequired),
766+
_ => None,
767+
});
768+
}
769+
770+
(username, connection_type, password_flag)
570771
})
571-
.flatten()
572772
.unwrap_or_default();
573773

574774
let path = conn.inner().path().to_owned();
@@ -579,6 +779,8 @@ fn connection_settings(conn: zbus::Connection) -> Command<crate::app::Message> {
579779
path,
580780
id,
581781
connection_type,
782+
password_flag,
783+
username,
582784
},
583785
))
584786
})

i18n/en/cosmic_settings.ftl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,19 @@ disconnect = Disconnect
2424
known-networks = Known Networks
2525
network-and-wireless = Network & Wireless
2626
no-networks = No networks have been found.
27+
password = Password
28+
remove = Remove
2729
settings = Settings
30+
username = Username
2831
visible-networks = Visible Networks
2932
33+
auth-dialog = Authentication Required
34+
.vpn-description = Enter the username and password required by the VPN service.
35+
.wifi-description = Enter the password or encryption key. You can also connect by pressing the “WPS” button on the router.
36+
37+
remove-connection-dialog = Remove Connection Profile?
38+
.description = You'll need to enter a password again to use this network in the future.
39+
3040
vpn = VPN
3141
.connections = VPN Connections
3242
.remove = Remove connection profile

0 commit comments

Comments
 (0)