diff options
author | Jae-Heon Ji <32578710+jaeheonji@users.noreply.github.com> | 2023-08-12 22:35:42 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-08-12 15:35:42 +0200 |
commit | c8ddb23297e2f4fc900b8286d57e2808ae6a4fdb (patch) | |
tree | c96d26c8bfc24172374aefe88624b78c6890663f /zellij-server | |
parent | a1903b6b048f8257fc16ffd09e19c825248cb9d6 (diff) |
feat: add plugin permission system (#2624)
* WIP: add exaple of permission ui
* feat: add request permission ui
* feat: add caching permission in memory
* feat: add permission check
* feat: add file caching
* fix: changes request
* feat(ui): new status bar mode (#2619)
* supermode prototype
* fix integration tests
* fix tests
* style(fmt): rustfmt
* docs(changelog): status-bar supermode
* fix(rendering): occasional glitches while resizing (#2621)
* docs(changelog): resize glitches fix
* chore(version): bump development version
* Fix colored pane frames in mirrored sessions (#2625)
* server/panes/tiled: Fix colored frames
in mirrored sessions. Colored frames were previously ignored because
they were treated like floating panes when rendering tiled panes.
* CHANGELOG: Add PR #2625
* server/tab/unit: Fix unit tests for server.
* fix(sessions): use custom lists of adjectives and nouns for generating session names (#2122)
* Create custom lists of adjectives and nouns for generating session names
* move word lists to const slices
* add logic to retry name generation
* refactor
- reuse the name generator
- iterator instead of for loop
---------
Co-authored-by: Thomas Linford <linford.t@gmail.com>
* docs(changelog): generate session names with custom words list
* feat(plugins): make plugins configurable (#2646)
* work
* make every plugin entry point configurable
* make integration tests pass
* make e2e tests pass
* add test for plugin configuration
* add test snapshot
* add plugin config parsing test
* cleanups
* style(fmt): rustfmt
* style(comment): remove commented code
* docs(changelog): configurable plugins
* style(fmt): rustfmt
* touch up ui
* fix: don't save permission data in memory
* feat: load cached permission
* test: add example test (WIP)
* fix: issue event are always denied
* test: update snapshot
* apply formatting
* refactor: update default cache function
* test: add more new test
* apply formatting
* Revert "apply formatting"
This reverts commit a4e93703fbfdb6865131daa1c8b90fc5c36ab25e.
* apply format
* fix: update cache path
* apply format
* fix: cache path
* fix: update log level
* test for github workflow
* Revert "test for github workflow"
This reverts commit 01eff3bc5d1627a4e60bc6dac8ebe5500bc5b56e.
* refactor: permission cache
* fix(test): permission grant/deny race condition
* style(fmt): rustfmt
* style(fmt): rustfmt
* configure permissions
* permission denied test
* snapshot
* add ui for small plugins
* style(fmt): rustfmt
* some cleanups
---------
Co-authored-by: Aram Drevekenin <aram@poor.dev>
Co-authored-by: har7an <99636919+har7an@users.noreply.github.com>
Co-authored-by: Kyle Sutherland-Cash <kyle.sutherlandcash@gmail.com>
Co-authored-by: Thomas Linford <linford.t@gmail.com>
Co-authored-by: Thomas Linford <tlinford@users.noreply.github.com>
Diffstat (limited to 'zellij-server')
15 files changed, 1599 insertions, 327 deletions
diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs index 2de5b74eb..1f2d770bb 100644 --- a/zellij-server/src/panes/floating_panes/mod.rs +++ b/zellij-server/src/panes/floating_panes/mod.rs @@ -295,7 +295,7 @@ impl FloatingPanes { pane.render_full_viewport(); } } - pub fn set_pane_frames(&mut self, os_api: &mut Box<dyn ServerOsApi>) -> Result<()> { + pub fn set_pane_frames(&mut self, _os_api: &mut Box<dyn ServerOsApi>) -> Result<()> { let err_context = |pane_id: &PaneId| format!("failed to activate frame on pane {pane_id:?}"); @@ -392,7 +392,7 @@ impl FloatingPanes { self.set_force_render(); } - pub fn resize_pty_all_panes(&mut self, os_api: &mut Box<dyn ServerOsApi>) -> Result<()> { + pub fn resize_pty_all_panes(&mut self, _os_api: &mut Box<dyn ServerOsApi>) -> Result<()> { for pane in self.panes.values_mut() { resize_pty!(pane, os_api, self.senders, self.character_cell_size) .with_context(|| format!("failed to resize PTY in pane {:?}", pane.pid()))?; @@ -403,7 +403,7 @@ impl FloatingPanes { pub fn resize_active_pane( &mut self, client_id: ClientId, - os_api: &mut Box<dyn ServerOsApi>, + _os_api: &mut Box<dyn ServerOsApi>, strategy: &ResizeStrategy, ) -> Result<bool> { // true => successfully resized @@ -838,7 +838,7 @@ impl FloatingPanes { self.focus_pane_for_all_clients(focused_pane); } } - pub fn switch_active_pane_with(&mut self, os_api: &mut Box<dyn ServerOsApi>, pane_id: PaneId) { + pub fn switch_active_pane_with(&mut self, _os_api: &mut Box<dyn ServerOsApi>, pane_id: PaneId) { if let Some(active_pane_id) = self.first_active_floating_pane_id() { let current_position = self.panes.get(&active_pane_id).unwrap(); let prev_geom = current_position.position_and_size(); diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index 465529609..b4e3461e2 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -1,11 +1,11 @@ -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use std::time::Instant; use crate::output::{CharacterChunk, SixelImageChunk}; use crate::panes::{grid::Grid, sixel::SixelImageStore, LinkHandler, PaneId}; use crate::plugins::PluginInstruction; use crate::pty::VteBytes; -use crate::tab::Pane; +use crate::tab::{AdjustedInput, Pane}; use crate::ui::{ loading_indication::LoadingIndication, pane_boundaries_frame::{FrameParams, PaneFrame}, @@ -13,6 +13,7 @@ use crate::ui::{ use crate::ClientId; use std::cell::RefCell; use std::rc::Rc; +use zellij_utils::data::{PermissionStatus, PermissionType, PluginPermission}; use zellij_utils::pane_size::{Offset, SizeInPixels}; use zellij_utils::position::Position; use zellij_utils::{ @@ -25,6 +26,15 @@ use zellij_utils::{ vte, }; +macro_rules! style { + ($fg:expr) => { + ansi_term::Style::new().fg(match $fg { + PaletteColor::Rgb((r, g, b)) => ansi_term::Color::RGB(r, g, b), + PaletteColor::EightBit(color) => ansi_term::Color::Fixed(color), + }) + }; +} + macro_rules! get_or_create_grid { ($self:ident, $client_id:ident) => {{ let rows = $self.get_content_rows(); @@ -73,6 +83,7 @@ pub(crate) struct PluginPane { pane_frame_color_override: Option<(PaletteColor, Option<String>)>, invoked_with: Option<Run>, loading_indication: LoadingIndication, + requesting_permissions: Option<PluginPermission>, debug: bool, } @@ -121,6 +132,7 @@ impl PluginPane { pane_frame_color_override: None, invoked_with, loading_indication, + requesting_permissions: None, debug, }; for client_id in currently_connected_clients { @@ -181,6 +193,14 @@ impl Pane for PluginPane { } fn handle_plugin_bytes(&mut self, client_id: ClientId, bytes: VteBytes) { self.set_client_should_render(client_id, true); + + let mut vte_bytes = bytes; + if let Some(plugin_permission) = &self.requesting_permissions { + vte_bytes = self + .display_request_permission_message(plugin_permission) + .into(); + } + let grid = get_or_create_grid!(self, client_id); // this is part of the plugin contract, whenever we update the plugin and call its render function, we delete the existing viewport @@ -193,14 +213,36 @@ impl Pane for PluginPane { .vte_parsers .entry(client_id) .or_insert_with(|| vte::Parser::new()); - for &byte in &bytes { + + for &byte in &vte_bytes { vte_parser.advance(grid, byte); } + self.should_render.insert(client_id, true); } fn cursor_coordinates(&self) -> Option<(usize, usize)> { None } + fn adjust_input_to_terminal(&mut self, input_bytes: Vec<u8>) -> Option<AdjustedInput> { + if let Some(requesting_permissions) = &self.requesting_permissions { + let permissions = requesting_permissions.permissions.clone(); + match input_bytes.as_slice() { + // Y or y + &[89] | &[121] => Some(AdjustedInput::PermissionRequestResult( + permissions, + PermissionStatus::Granted, + )), + // N or n + &[78] | &[110] => Some(AdjustedInput::PermissionRequestResult( + permissions, + PermissionStatus::Denied, + )), + _ => None, + } + } else { + Some(AdjustedInput::WriteBytesToTerminal(input_bytes)) + } + } fn position_and_size(&self) -> PaneGeom { self.geom } @@ -233,6 +275,9 @@ impl Pane for PluginPane { fn set_selectable(&mut self, selectable: bool) { self.selectable = selectable; } + fn request_permissions_from_user(&mut self, permissions: Option<PluginPermission>) { + self.requesting_permissions = permissions; + } fn render( &mut self, client_id: Option<ClientId>, @@ -595,4 +640,54 @@ impl PluginPane { self.handle_plugin_bytes(client_id, bytes.clone()); } } + fn display_request_permission_message(&self, plugin_permission: &PluginPermission) -> String { + let bold_white = style!(self.style.colors.white).bold(); + let cyan = style!(self.style.colors.cyan).bold(); + let orange = style!(self.style.colors.orange).bold(); + let green = style!(self.style.colors.green).bold(); + + let mut messages = String::new(); + let permissions: BTreeSet<PermissionType> = + plugin_permission.permissions.clone().into_iter().collect(); + + let min_row_count = permissions.len() + 4; + + if self.rows() >= min_row_count { + messages.push_str(&format!( + "{} {} {}\n", + bold_white.paint("Plugin"), + cyan.paint(&plugin_permission.name), + bold_white.paint("asks permission to:"), + )); + permissions.iter().enumerate().for_each(|(i, p)| { + messages.push_str(&format!( + "\n\r{}. {}", + bold_white.paint(&format!("{}", i + 1)), + orange.paint(p.display_name()) + )); + }); + + messages.push_str(&format!( + "\n\n\r{} {}", + bold_white.paint("Allow?"), + green.paint("(y/n)"), + )); + } else { + messages.push_str(&format!( + "{} {}. {} {}\n", + bold_white.paint("This plugin asks permission to:"), + orange.paint( + permissions + .iter() + .map(|p| p.to_string()) + .collect::<Vec<_>>() + .join(", ") + ), + bold_white.paint("Allow?"), + green.paint("(y/n)"), + )); + } + + messages + } } diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs index 9aca1af4a..736c16bc9 100644 --- a/zellij-server/src/panes/tiled_panes/mod.rs +++ b/zellij-server/src/panes/tiled_panes/mod.rs @@ -68,7 +68,6 @@ pub struct TiledPanes { draw_pane_frames: bool, panes_to_hide: HashSet<PaneId>, fullscreen_is_active: bool, - os_api: Box<dyn ServerOsApi>, senders: ThreadSenders, window_title: Option<String>, client_id_to_boundaries: HashMap<ClientId, Boundaries>, @@ -105,7 +104,6 @@ impl TiledPanes { draw_pane_frames, panes_to_hide: HashSet::new(), fullscreen_is_active: false, - os_api, senders, window_title: None, client_id_to_boundaries: HashMap::new(), diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs index f36bdc2d2..4bef0683d 100644 --- a/zellij-server/src/plugins/mod.rs +++ b/zellij-server/src/plugins/mod.rs @@ -18,7 +18,7 @@ use crate::{pty::PtyInstruction, thread_bus::Bus, ClientId, ServerInstruction}; use wasm_bridge::WasmBridge; use zellij_utils::{ - data::{Event, EventType, PluginCapabilities}, + data::{Event, EventType, PermissionStatus, PermissionType, PluginCapabilities}, errors::{prelude::*, ContextType, PluginContext}, input::{ command::TerminalAction, @@ -79,6 +79,13 @@ pub enum PluginInstruction { String, // serialized payload ), PluginSubscribedToEvents(PluginId, ClientId, HashSet<EventType>), + PermissionRequestResult( + PluginId, + Option<ClientId>, + Vec<PermissionType>, + PermissionStatus, + Option<PathBuf>, + ), Exit, } @@ -105,6 +112,9 @@ impl From<&PluginInstruction> for PluginContext { PluginInstruction::PluginSubscribedToEvents(..) => { PluginContext::PluginSubscribedToEvents }, + PluginInstruction::PermissionRequestResult(..) => { + PluginContext::PermissionRequestResult + }, } } } @@ -287,6 +297,30 @@ pub(crate) fn plugin_thread_main( } } }, + PluginInstruction::PermissionRequestResult( + plugin_id, + client_id, + permissions, + status, + cache_path, + ) => { + if let Err(e) = wasm_bridge.cache_plugin_permissions( + plugin_id, + client_id, + permissions, + status, + cache_path, + ) { + log::error!("{}", e); + } + + let updates = vec![( + Some(plugin_id), + client_id, + Event::PermissionRequestResult(status), + )]; + wasm_bridge.update_plugins(updates)?; + }, PluginInstruction::Exit => { wasm_bridge.cleanup(); break; diff --git a/zellij-server/src/plugins/plugin_loader.rs b/zellij-server/src/plugins/plugin_loader.rs index 92a4c0480..ea0c2b6a6 100644 --- a/zellij-server/src/plugins/plugin_loader.rs +++ b/zellij-server/src/plugins/plugin_loader.rs @@ -188,6 +188,7 @@ impl<'a> PluginLoader<'a> { display_loading_stage!(end, loading_indication, senders, plugin_id); Ok(()) } + pub fn add_client( client_id: ClientId, plugin_dir: PathBuf, @@ -613,6 +614,19 @@ impl<'a> PluginLoader<'a> { } start_function.call(&[]).with_context(err_context)?; + plugin_map.lock().unwrap().insert( + self.plugin_id, + self.client_id, + Arc::new(Mutex::new(RunningPlugin::new( + main_user_instance, + main_user_env, + self.size.rows, + self.size.cols, + ))), + subscriptions.clone(), + workers, + ); + let protobuf_plugin_configuration: ProtobufPluginConfiguration = self .plugin .userspace_configuration @@ -640,18 +654,6 @@ impl<'a> PluginLoader<'a> { self.senders, self.plugin_id ); - plugin_map.lock().unwrap().insert( - self.plugin_id, - self.client_id, - Arc::new(Mutex::new(RunningPlugin::new( - main_user_instance, - main_user_env, - self.size.rows, - self.size.cols, - ))), - subscriptions.clone(), - workers, - ); display_loading_stage!( indicate_writing_plugin_to_cache_success, self.loading_indication, @@ -764,13 +766,13 @@ impl<'a> PluginLoader<'a> { }) .with_context(err_context)?; let wasi = wasi_env.import_object(&module).with_context(err_context)?; - let mut mut_plugin = self.plugin.clone(); mut_plugin.set_tab_index(self.tab_index); let plugin_env = PluginEnv { plugin_id: self.plugin_id, client_id: self.client_id, plugin: mut_plugin, + permissions: Arc::new(Mutex::new(None)), senders: self.senders.clone(), wasi_env, plugin_own_data_dir: self.plugin_own_data_dir.clone(), diff --git a/zellij-server/src/plugins/plugin_map.rs b/zellij-server/src/plugins/plugin_map.rs index 2f7454c7e..0cf26943e 100644 --- a/zellij-server/src/plugins/plugin_map.rs +++ b/zellij-server/src/plugins/plugin_map.rs @@ -11,7 +11,6 @@ use wasmer_wasi::WasiEnv; use crate::{thread_bus::ThreadSenders, ClientId}; use zellij_utils::async_channel::Sender; -use zellij_utils::errors::prelude::*; use zellij_utils::{ data::EventType, data::PluginCapabilities, @@ -20,6 +19,7 @@ use zellij_utils::{ input::plugins::PluginConfig, ipc::ClientAttributes, }; +use zellij_utils::{data::PermissionType, errors::prelude::*}; // the idea here is to provide atomicity when adding/removing plugins from the map (eg. when a new // client connects) but to also allow updates/renders not to block each other @@ -193,6 +193,7 @@ pub type Subscriptions = HashSet<EventType>; pub struct PluginEnv { pub plugin_id: PluginId, pub plugin: PluginConfig, + pub permissions: Arc<Mutex<Option<HashSet<PermissionType>>>>, pub senders: ThreadSenders, pub wasi_env: WasiEnv, pub tab_index: usize, @@ -215,6 +216,10 @@ impl PluginEnv { self.plugin_id ) } + + pub fn set_permissions(&mut self, permissions: HashSet<PermissionType>) { + self.permissions.lock().unwrap().replace(permissions); + } } #[derive(Eq, PartialEq, Hash)] diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index 663ca94ee..e995a07b9 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -6,9 +6,10 @@ use std::collections::BTreeMap; use std::path::PathBuf; use tempfile::tempdir; use wasmer::Store; -use zellij_utils::data::{Event, Key, PluginCapabilities}; +use zellij_utils::data::{Event, Key, PermissionStatus, PermissionType, PluginCapabilities}; use zellij_utils::errors::ErrorContext; use zellij_utils::input::layout::{Layout, PluginUserConfiguration, RunPlugin, RunPluginLocation}; +use zellij_utils::input::permission::PermissionCache; use zellij_utils::input::plugins::PluginsConfig; use zellij_utils::ipc::ClientAttributes; use zellij_utils::lazy_static::lazy_static; @@ -52,6 +53,157 @@ macro_rules! log_actions_in_thread { }; } +macro_rules! grant_permissions_and_log_actions_in_thread { + ( $arc_mutex_log:expr, $exit_event:path, $receiver:expr, $exit_after_count:expr, $permission_type:expr, $cache_path:expr, $plugin_thread_sender:expr, $client_id:expr ) => { + std::thread::Builder::new() + .name("fake_screen_thread".to_string()) + .spawn({ + let log = $arc_mutex_log.clone(); + let mut exit_event_count = 0; + let cache_path = $cache_path.clone(); + let plugin_thread_sender = $plugin_thread_sender.clone(); + move || loop { + let (event, _err_ctx) = $receiver + .recv() + .expect("failed to receive event on channel"); + match event { + $exit_event(..) => { + exit_event_count += 1; + log.lock().unwrap().push(event); + if exit_event_count == $exit_after_count { + break; + } + }, + ScreenInstruction::RequestPluginPermissions(_, plugin_permission) => { + if plugin_permission.permissions.contains($permission_type) { + let _ = plugin_thread_sender.send( + PluginInstruction::PermissionRequestResult( + 0, + Some($client_id), + plugin_permission.permissions, + PermissionStatus::Granted, + Some(cache_path.clone()), + ), + ); + } else { + let _ = plugin_thread_sender.send( + PluginInstruction::PermissionRequestResult( + 0, + Some($client_id), + plugin_permission.permissions, + PermissionStatus::Denied, + Some(cache_path.clone()), + ), + ); + } + }, + _ => { + log.lock().unwrap().push(event); + }, + } + } + }) + .unwrap() + }; +} + +macro_rules! deny_permissions_and_log_actions_in_thread { + ( $arc_mutex_log:expr, $exit_event:path, $receiver:expr, $exit_after_count:expr, $permission_type:expr, $cache_path:expr, $plugin_thread_sender:expr, $client_id:expr ) => { + std::thread::Builder::new() + .name("fake_screen_thread".to_string()) + .spawn({ + let log = $arc_mutex_log.clone(); + let mut exit_event_count = 0; + let cache_path = $cache_path.clone(); + let plugin_thread_sender = $plugin_thread_sender.clone(); + move || loop { + let (event, _err_ctx) = $receiver + .recv() + .expect("failed to receive event on channel"); + match event { + $exit_event(..) => { + exit_event_count += 1; + log.lock().unwrap().push(event); + if exit_event_count == $exit_after_count { + break; + } + }, + ScreenInstruction::RequestPluginPermissions(_, plugin_permission) => { + let _ = plugin_thread_sender.send( + PluginInstruction::PermissionRequestResult( + 0, + |