summaryrefslogtreecommitdiffstats
path: root/zellij-server
diff options
context:
space:
mode:
authorJae-Heon Ji <32578710+jaeheonji@users.noreply.github.com>2023-08-12 22:35:42 +0900
committerGitHub <noreply@github.com>2023-08-12 15:35:42 +0200
commitc8ddb23297e2f4fc900b8286d57e2808ae6a4fdb (patch)
treec96d26c8bfc24172374aefe88624b78c6890663f /zellij-server
parenta1903b6b048f8257fc16ffd09e19c825248cb9d6 (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')
-rw-r--r--zellij-server/src/panes/floating_panes/mod.rs8
-rw-r--r--zellij-server/src/panes/plugin_pane.rs101
-rw-r--r--zellij-server/src/panes/tiled_panes/mod.rs2
-rw-r--r--zellij-server/src/plugins/mod.rs36
-rw-r--r--zellij-server/src/plugins/plugin_loader.rs28
-rw-r--r--zellij-server/src/plugins/plugin_map.rs7
-rw-r--r--zellij-server/src/plugins/unit/plugin_tests.rs1151
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__denied_permission_request_result.snap7
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap15
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap17
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command_permission_denied.snap6
-rw-r--r--zellij-server/src/plugins/wasm_bridge.rs138
-rw-r--r--zellij-server/src/plugins/zellij_exports.rs329
-rw-r--r--zellij-server/src/screen.rs27
-rw-r--r--zellij-server/src/tab/mod.rs54
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,
+