feat(gui-clients): permit resource enable and disable (#6248)

Last PR for #6074

This adds Enable/Disable for tauri clients.

In windows, edge seems to hold on to the sockets for a bit too long
after disabling the resources. This will be solved for the internet
resource probably by modifying the firewall, in another PR.
This commit is contained in:
Gabi
2024-08-16 00:41:15 -03:00
committed by GitHub
parent 417de82b8c
commit df4d604ad3
6 changed files with 143 additions and 53 deletions

View File

@@ -70,6 +70,14 @@ impl ResourceDescription {
ResourceDescription::Internet(r) => &r.sites,
}
}
pub fn can_toggle(&self) -> bool {
match self {
ResourceDescription::Dns(r) => r.can_toggle,
ResourceDescription::Cidr(r) => r.can_toggle,
ResourceDescription::Internet(r) => r.can_toggle,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]

View File

@@ -10,7 +10,10 @@ use crate::client::{
};
use anyhow::{anyhow, bail, Context, Result};
use firezone_bin_shared::{new_dns_notifier, new_network_notifier};
use firezone_headless_client::{IpcClientMsg, IpcServerMsg};
use firezone_headless_client::{
IpcClientMsg::{self, SetDisabledResources},
IpcServerMsg,
};
use secrecy::{ExposeSecret, SecretString};
use std::{
path::PathBuf,
@@ -621,6 +624,14 @@ impl Controller {
self.advanced_settings.favorite_resources.remove(&resource_id);
self.refresh_favorite_resources().await?;
}
Req::SystemTrayMenu(TrayMenuEvent::EnableResource(resource_id)) => {
self.advanced_settings.disabled_resources.remove(&resource_id);
self.update_disabled_resources().await?;
}
Req::SystemTrayMenu(TrayMenuEvent::DisableResource(resource_id)) => {
self.advanced_settings.disabled_resources.insert(resource_id);
self.update_disabled_resources().await?;
}
Req::SystemTrayMenu(TrayMenuEvent::ShowWindow(window)) => {
self.show_window(window)?;
// When the About or Settings windows are hidden / shown, log the
@@ -716,6 +727,9 @@ impl Controller {
if let Err(error) = self.refresh_system_tray_menu() {
tracing::error!(?error, "Failed to refresh Resource list");
}
self.update_disabled_resources().await?;
Ok(())
}
IpcServerMsg::TerminatingGracefully => {
@@ -726,6 +740,27 @@ impl Controller {
}
}
async fn update_disabled_resources(&mut self) -> Result<()> {
settings::save(&self.advanced_settings).await?;
let Status::TunnelReady { resources } = &self.status else {
bail!("Tunnel is not ready");
};
let disabled_resources = resources
.iter()
.filter_map(|r| r.can_toggle().then_some(r.id()))
.filter(|id| self.advanced_settings.disabled_resources.contains(id))
.collect();
self.ipc_client
.send_msg(&SetDisabledResources(disabled_resources))
.await?;
self.refresh_system_tray_menu()?;
Ok(())
}
/// Saves the current settings (including favorites) to disk and refreshes the tray menu
async fn refresh_favorite_resources(&mut self) -> Result<()> {
settings::save(&self.advanced_settings).await?;
@@ -748,6 +783,7 @@ impl Controller {
system_tray::AppState::SignedIn(system_tray::SignedIn {
actor_name: &auth_session.actor_name,
favorite_resources: &self.advanced_settings.favorite_resources,
disabled_resources: &self.advanced_settings.disabled_resources,
resources,
})
}

View File

@@ -37,6 +37,8 @@ const RESOURCES: &str = "Resources";
const OTHER_RESOURCES: &str = "Other Resources";
const SIGN_OUT: &str = "Sign out";
const DISCONNECT_AND_QUIT: &str = "Disconnect and quit Firezone";
const DISABLE: &str = "Disable this resource";
const ENABLE: &str = "Enable this resource";
pub(crate) fn loading() -> SystemTray {
SystemTray::new()
@@ -62,6 +64,7 @@ pub(crate) struct SignedIn<'a> {
pub(crate) actor_name: &'a str,
pub(crate) favorite_resources: &'a HashSet<ResourceId>,
pub(crate) resources: &'a [ResourceDescription],
pub(crate) disabled_resources: &'a HashSet<ResourceId>,
}
impl<'a> SignedIn<'a> {
@@ -72,19 +75,30 @@ impl<'a> SignedIn<'a> {
/// Builds the submenu that has the resource address, name, desc,
/// sites online, etc.
fn resource_submenu(&self, res: &ResourceDescription) -> Menu {
let submenu = Menu::default().add_item(resource_header(res));
let mut submenu = Menu::default();
let submenu = submenu
submenu.add_item(resource_header(res));
let mut submenu = submenu
.separator()
.disabled("Resource")
.copyable(res.name())
.copyable(res.pastable().as_ref());
let submenu = if self.is_favorite(res) {
submenu.add_item(item(Event::RemoveFavorite(res.id()), REMOVE_FAVORITE).selected())
if self.is_favorite(res) {
submenu.add_item(item(Event::RemoveFavorite(res.id()), REMOVE_FAVORITE).selected());
} else {
submenu.item(Event::AddFavorite(res.id()), ADD_FAVORITE)
};
submenu.add_item(item(Event::AddFavorite(res.id()), ADD_FAVORITE));
}
if res.can_toggle() {
submenu.add_separator();
if self.is_enabled(res) {
submenu.add_item(item(Event::DisableResource(res.id()), DISABLE));
} else {
submenu.add_item(item(Event::EnableResource(res.id()), ENABLE));
}
}
if let Some(site) = res.sites().first() {
// Emojis may be causing an issue on some Ubuntu desktop environments.
@@ -103,6 +117,10 @@ impl<'a> SignedIn<'a> {
submenu
}
}
fn is_enabled(&self, res: &ResourceDescription) -> bool {
!self.disabled_resources.contains(&res.id())
}
}
fn resource_header(res: &ResourceDescription) -> Item {
@@ -207,6 +225,7 @@ fn signed_in(signed_in: &SignedIn) -> Menu {
actor_name,
favorite_resources,
resources, // Make sure these are presented in the order we receive them
..
} = signed_in;
let has_any_favorites = resources
@@ -270,6 +289,30 @@ mod tests {
use anyhow::Result;
use std::str::FromStr as _;
impl Menu {
fn selected_item<E: Into<Option<Event>>, S: Into<String>>(
mut self,
id: E,
title: S,
) -> Self {
self.add_item(item(id, title).selected());
self
}
}
fn signed_in<'a>(
resources: &'a [ResourceDescription],
favorite_resources: &'a HashSet<ResourceId>,
disabled_resources: &'a HashSet<ResourceId>,
) -> AppState<'a> {
AppState::SignedIn(SignedIn {
actor_name: "Jane Doe",
favorite_resources,
resources,
disabled_resources,
})
}
fn resources() -> Vec<ResourceDescription> {
let s = r#"[
{
@@ -311,11 +354,8 @@ mod tests {
fn no_resources_no_favorites() {
let resources = vec![];
let favorites = Default::default();
let input = AppState::SignedIn(SignedIn {
actor_name: "Jane Doe",
favorite_resources: &favorites,
resources: &resources,
});
let disabled_resources = Default::default();
let input = signed_in(&resources, &favorites, &disabled_resources);
let actual = input.into_menu();
let expected = Menu::default()
.disabled("Signed in as Jane Doe")
@@ -336,11 +376,8 @@ mod tests {
fn no_resources_invalid_favorite() {
let resources = vec![];
let favorites = HashSet::from([ResourceId::from_u128(42)]);
let input = AppState::SignedIn(SignedIn {
actor_name: "Jane Doe",
favorite_resources: &favorites,
resources: &resources,
});
let disabled_resources = Default::default();
let input = signed_in(&resources, &favorites, &disabled_resources);
let actual = input.into_menu();
let expected = Menu::default()
.disabled("Signed in as Jane Doe")
@@ -361,11 +398,8 @@ mod tests {
fn some_resources_no_favorites() {
let resources = resources();
let favorites = Default::default();
let input = AppState::SignedIn(SignedIn {
actor_name: "Jane Doe",
favorite_resources: &favorites,
resources: &resources,
});
let disabled_resources = Default::default();
let input = signed_in(&resources, &favorites, &disabled_resources);
let actual = input.into_menu();
let expected = Menu::default()
.disabled("Signed in as Jane Doe")
@@ -447,11 +481,8 @@ mod tests {
let favorites = HashSet::from([ResourceId::from_str(
"03000143-e25e-45c7-aafb-144990e57dcd",
)?]);
let input = AppState::SignedIn(SignedIn {
actor_name: "Jane Doe",
favorite_resources: &favorites,
resources: &resources,
});
let disabled_resources = Default::default();
let input = signed_in(&resources, &favorites, &disabled_resources);
let actual = input.into_menu();
let expected = Menu::default()
.disabled("Signed in as Jane Doe")
@@ -469,14 +500,11 @@ mod tests {
.disabled("Resource")
.copyable("MyCorp GitLab")
.copyable("gitlab.mycorp.com")
.add_item(
item(
Event::RemoveFavorite(ResourceId::from_str(
"03000143-e25e-45c7-aafb-144990e57dcd",
)?),
REMOVE_FAVORITE,
)
.selected(),
.selected_item(
Event::RemoveFavorite(ResourceId::from_str(
"03000143-e25e-45c7-aafb-144990e57dcd",
)?),
REMOVE_FAVORITE,
)
.separator()
.disabled("Site")
@@ -544,11 +572,8 @@ mod tests {
let favorites = HashSet::from([ResourceId::from_str(
"00000000-0000-0000-0000-000000000000",
)?]);
let input = AppState::SignedIn(SignedIn {
actor_name: "Jane Doe",
favorite_resources: &favorites,
resources: &resources,
});
let disabled_resources = Default::default();
let input = signed_in(&resources, &favorites, &disabled_resources);
let actual = input.into_menu();
let expected = Menu::default()
.disabled("Signed in as Jane Doe")

View File

@@ -63,6 +63,10 @@ pub(crate) enum Event {
Url(Url),
/// Quits the app, without signing the user out
Quit,
/// A resource was enabled in the UI
EnableResource(ResourceId),
/// A resource was disabled in the UI
DisableResource(ResourceId),
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
@@ -94,9 +98,12 @@ impl Menu {
.item(Event::Quit, quit_text)
}
pub(crate) fn add_item(mut self, item: Item) -> Self {
pub(crate) fn add_separator(&mut self) {
self.entries.push(Entry::Separator);
}
pub(crate) fn add_item(&mut self, item: Item) {
self.entries.push(Entry::Item(item));
self
}
pub(crate) fn add_submenu<S: Into<String>>(mut self, title: S, inner: Menu) -> Self {
@@ -125,23 +132,26 @@ impl Menu {
}
/// Appends a menu item that copies its title when clicked
pub(crate) fn copyable(self, s: &str) -> Self {
self.add_item(copyable(s))
pub(crate) fn copyable(mut self, s: &str) -> Self {
self.add_item(copyable(s));
self
}
/// Appends a disabled item with no accelerator or event
pub(crate) fn disabled<S: Into<String>>(self, title: S) -> Self {
self.add_item(item(None, title).disabled())
pub(crate) fn disabled<S: Into<String>>(mut self, title: S) -> Self {
self.add_item(item(None, title).disabled());
self
}
/// Appends a generic menu item
pub(crate) fn item<E: Into<Option<Event>>, S: Into<String>>(self, id: E, title: S) -> Self {
self.add_item(item(id, title))
pub(crate) fn item<E: Into<Option<Event>>, S: Into<String>>(mut self, id: E, title: S) -> Self {
self.add_item(item(id, title));
self
}
/// Appends a separator
pub(crate) fn separator(mut self) -> Self {
self.entries.push(Entry::Separator);
self.add_separator();
self
}
}

View File

@@ -15,9 +15,10 @@ use url::Url;
pub(crate) struct AdvancedSettings {
pub auth_base_url: Url,
pub api_url: Url,
// TODO: Will this cause problems when we add the ability to enable / disable Resources on the client side? At least the Internet Resource will need that.
#[serde(default)]
pub favorite_resources: HashSet<ResourceId>,
#[serde(default)]
pub disabled_resources: HashSet<ResourceId>,
pub log_filter: String,
}
@@ -28,6 +29,7 @@ impl Default for AdvancedSettings {
auth_base_url: Url::parse("https://app.firez.one").unwrap(),
api_url: Url::parse("wss://api.firez.one").unwrap(),
favorite_resources: Default::default(),
disabled_resources: Default::default(),
log_filter: "firezone_gui_client=debug,info".to_string(),
}
}
@@ -40,6 +42,7 @@ impl Default for AdvancedSettings {
auth_base_url: Url::parse("https://app.firezone.dev").unwrap(),
api_url: Url::parse("wss://api.firezone.dev").unwrap(),
favorite_resources: Default::default(),
disabled_resources: Default::default(),
log_filter: "info".to_string(),
}
}

View File

@@ -14,7 +14,7 @@ use futures::{
task::{Context, Poll},
Future as _, SinkExt as _, Stream as _,
};
use std::{net::IpAddr, path::PathBuf, pin::pin, sync::Arc, time::Duration};
use std::{collections::HashSet, net::IpAddr, path::PathBuf, pin::pin, sync::Arc, time::Duration};
use tokio::{sync::mpsc, time::Instant};
use tracing::subscriber::set_global_default;
use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Layer, Registry};
@@ -22,7 +22,7 @@ use url::Url;
pub mod ipc;
use backoff::ExponentialBackoffBuilder;
use connlib_shared::{get_user_agent, DEFAULT_MTU};
use connlib_shared::{get_user_agent, messages::ResourceId, DEFAULT_MTU};
use ipc::{Server as IpcServer, ServiceId};
use phoenix_channel::PhoenixChannel;
use secrecy::Secret;
@@ -76,6 +76,7 @@ pub enum ClientMsg {
Disconnect,
Reset,
SetDns(Vec<IpAddr>),
SetDisabledResources(HashSet<ResourceId>),
}
/// Only called from the GUI Client's build of the IPC service
@@ -343,7 +344,8 @@ impl<'a> Handler<'a> {
.context("Error while sending IPC message `OnUpdateResources`")?;
}
ConnlibMsg::OnUpdateRoutes { ipv4, ipv6 } => {
self.tun_device.set_routes(ipv4, ipv6).await?
self.tun_device.set_routes(ipv4, ipv6).await?;
self.dns_controller.flush()?;
}
}
Ok(())
@@ -420,6 +422,12 @@ impl<'a> Handler<'a> {
.as_mut()
.context("No connlib session")?
.set_dns(v),
ClientMsg::SetDisabledResources(disabled_resources) => {
self.connlib
.as_mut()
.context("No connlib session")?
.set_disabled_resources(disabled_resources);
}
}
Ok(())
}