mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
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:
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user