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>
This commit is contained in:
Jae-Heon Ji 2023-08-12 22:35:42 +09:00 committed by GitHub
parent a1903b6b04
commit c8ddb23297
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1935 additions and 337 deletions

View file

@ -39,6 +39,16 @@ register_worker!(TestWorker, test_worker, TEST_WORKER);
impl ZellijPlugin for State { impl ZellijPlugin for State {
fn load(&mut self, configuration: BTreeMap<String, String>) { fn load(&mut self, configuration: BTreeMap<String, String>) {
request_permission(&[
PermissionType::ChangeApplicationState,
PermissionType::ReadApplicationState,
PermissionType::ReadApplicationState,
PermissionType::ChangeApplicationState,
PermissionType::OpenFiles,
PermissionType::RunCommands,
PermissionType::OpenTerminalsOrPlugins,
PermissionType::WriteToStdin,
]);
self.configuration = configuration; self.configuration = configuration;
subscribe(&[ subscribe(&[
EventType::InputReceived, EventType::InputReceived,
@ -227,6 +237,9 @@ impl ZellijPlugin for State {
Key::Ctrl('z') => { Key::Ctrl('z') => {
go_to_tab_name(&format!("{:?}", self.configuration)); go_to_tab_name(&format!("{:?}", self.configuration));
}, },
Key::Ctrl('1') => {
request_permission(&[PermissionType::ReadApplicationState]);
},
_ => {}, _ => {},
}, },
Event::CustomMessage(message, payload) => { Event::CustomMessage(message, payload) => {

View file

@ -30,6 +30,7 @@ impl ZellijPlugin for State {
EventType::FileSystemCreate, EventType::FileSystemCreate,
EventType::FileSystemUpdate, EventType::FileSystemUpdate,
EventType::FileSystemDelete, EventType::FileSystemDelete,
EventType::PermissionRequestResult,
]); ]);
post_message_to(PluginMessage { post_message_to(PluginMessage {
worker_name: Some("file_name_search".into()), worker_name: Some("file_name_search".into()),
@ -54,6 +55,9 @@ impl ZellijPlugin for State {
}; };
self.ev_history.push_back((event.clone(), Instant::now())); self.ev_history.push_back((event.clone(), Instant::now()));
match event { match event {
Event::PermissionRequestResult(_) => {
should_render = true;
},
Event::Timer(_elapsed) => { Event::Timer(_elapsed) => {
if self.search_state.loading { if self.search_state.loading {
set_timeout(0.5); set_timeout(0.5);

View file

@ -295,7 +295,7 @@ impl FloatingPanes {
pane.render_full_viewport(); 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 = let err_context =
|pane_id: &PaneId| format!("failed to activate frame on pane {pane_id:?}"); |pane_id: &PaneId| format!("failed to activate frame on pane {pane_id:?}");
@ -392,7 +392,7 @@ impl FloatingPanes {
self.set_force_render(); 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() { for pane in self.panes.values_mut() {
resize_pty!(pane, os_api, self.senders, self.character_cell_size) resize_pty!(pane, os_api, self.senders, self.character_cell_size)
.with_context(|| format!("failed to resize PTY in pane {:?}", pane.pid()))?; .with_context(|| format!("failed to resize PTY in pane {:?}", pane.pid()))?;
@ -403,7 +403,7 @@ impl FloatingPanes {
pub fn resize_active_pane( pub fn resize_active_pane(
&mut self, &mut self,
client_id: ClientId, client_id: ClientId,
os_api: &mut Box<dyn ServerOsApi>, _os_api: &mut Box<dyn ServerOsApi>,
strategy: &ResizeStrategy, strategy: &ResizeStrategy,
) -> Result<bool> { ) -> Result<bool> {
// true => successfully resized // true => successfully resized
@ -838,7 +838,7 @@ impl FloatingPanes {
self.focus_pane_for_all_clients(focused_pane); 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() { if let Some(active_pane_id) = self.first_active_floating_pane_id() {
let current_position = self.panes.get(&active_pane_id).unwrap(); let current_position = self.panes.get(&active_pane_id).unwrap();
let prev_geom = current_position.position_and_size(); let prev_geom = current_position.position_and_size();

View file

@ -1,11 +1,11 @@
use std::collections::HashMap; use std::collections::{BTreeSet, HashMap};
use std::time::Instant; use std::time::Instant;
use crate::output::{CharacterChunk, SixelImageChunk}; use crate::output::{CharacterChunk, SixelImageChunk};
use crate::panes::{grid::Grid, sixel::SixelImageStore, LinkHandler, PaneId}; use crate::panes::{grid::Grid, sixel::SixelImageStore, LinkHandler, PaneId};
use crate::plugins::PluginInstruction; use crate::plugins::PluginInstruction;
use crate::pty::VteBytes; use crate::pty::VteBytes;
use crate::tab::Pane; use crate::tab::{AdjustedInput, Pane};
use crate::ui::{ use crate::ui::{
loading_indication::LoadingIndication, loading_indication::LoadingIndication,
pane_boundaries_frame::{FrameParams, PaneFrame}, pane_boundaries_frame::{FrameParams, PaneFrame},
@ -13,6 +13,7 @@ use crate::ui::{
use crate::ClientId; use crate::ClientId;
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
use zellij_utils::data::{PermissionStatus, PermissionType, PluginPermission};
use zellij_utils::pane_size::{Offset, SizeInPixels}; use zellij_utils::pane_size::{Offset, SizeInPixels};
use zellij_utils::position::Position; use zellij_utils::position::Position;
use zellij_utils::{ use zellij_utils::{
@ -25,6 +26,15 @@ use zellij_utils::{
vte, 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 { macro_rules! get_or_create_grid {
($self:ident, $client_id:ident) => {{ ($self:ident, $client_id:ident) => {{
let rows = $self.get_content_rows(); let rows = $self.get_content_rows();
@ -73,6 +83,7 @@ pub(crate) struct PluginPane {
pane_frame_color_override: Option<(PaletteColor, Option<String>)>, pane_frame_color_override: Option<(PaletteColor, Option<String>)>,
invoked_with: Option<Run>, invoked_with: Option<Run>,
loading_indication: LoadingIndication, loading_indication: LoadingIndication,
requesting_permissions: Option<PluginPermission>,
debug: bool, debug: bool,
} }
@ -121,6 +132,7 @@ impl PluginPane {
pane_frame_color_override: None, pane_frame_color_override: None,
invoked_with, invoked_with,
loading_indication, loading_indication,
requesting_permissions: None,
debug, debug,
}; };
for client_id in currently_connected_clients { 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) { fn handle_plugin_bytes(&mut self, client_id: ClientId, bytes: VteBytes) {
self.set_client_should_render(client_id, true); 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); 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 // 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 .vte_parsers
.entry(client_id) .entry(client_id)
.or_insert_with(|| vte::Parser::new()); .or_insert_with(|| vte::Parser::new());
for &byte in &bytes {
for &byte in &vte_bytes {
vte_parser.advance(grid, byte); vte_parser.advance(grid, byte);
} }
self.should_render.insert(client_id, true); self.should_render.insert(client_id, true);
} }
fn cursor_coordinates(&self) -> Option<(usize, usize)> { fn cursor_coordinates(&self) -> Option<(usize, usize)> {
None 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 { fn position_and_size(&self) -> PaneGeom {
self.geom self.geom
} }
@ -233,6 +275,9 @@ impl Pane for PluginPane {
fn set_selectable(&mut self, selectable: bool) { fn set_selectable(&mut self, selectable: bool) {
self.selectable = selectable; self.selectable = selectable;
} }
fn request_permissions_from_user(&mut self, permissions: Option<PluginPermission>) {
self.requesting_permissions = permissions;
}
fn render( fn render(
&mut self, &mut self,
client_id: Option<ClientId>, client_id: Option<ClientId>,
@ -595,4 +640,54 @@ impl PluginPane {
self.handle_plugin_bytes(client_id, bytes.clone()); 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
}
} }

View file

@ -68,7 +68,6 @@ pub struct TiledPanes {
draw_pane_frames: bool, draw_pane_frames: bool,
panes_to_hide: HashSet<PaneId>, panes_to_hide: HashSet<PaneId>,
fullscreen_is_active: bool, fullscreen_is_active: bool,
os_api: Box<dyn ServerOsApi>,
senders: ThreadSenders, senders: ThreadSenders,
window_title: Option<String>, window_title: Option<String>,
client_id_to_boundaries: HashMap<ClientId, Boundaries>, client_id_to_boundaries: HashMap<ClientId, Boundaries>,
@ -105,7 +104,6 @@ impl TiledPanes {
draw_pane_frames, draw_pane_frames,
panes_to_hide: HashSet::new(), panes_to_hide: HashSet::new(),
fullscreen_is_active: false, fullscreen_is_active: false,
os_api,
senders, senders,
window_title: None, window_title: None,
client_id_to_boundaries: HashMap::new(), client_id_to_boundaries: HashMap::new(),

View file

@ -18,7 +18,7 @@ use crate::{pty::PtyInstruction, thread_bus::Bus, ClientId, ServerInstruction};
use wasm_bridge::WasmBridge; use wasm_bridge::WasmBridge;
use zellij_utils::{ use zellij_utils::{
data::{Event, EventType, PluginCapabilities}, data::{Event, EventType, PermissionStatus, PermissionType, PluginCapabilities},
errors::{prelude::*, ContextType, PluginContext}, errors::{prelude::*, ContextType, PluginContext},
input::{ input::{
command::TerminalAction, command::TerminalAction,
@ -79,6 +79,13 @@ pub enum PluginInstruction {
String, // serialized payload String, // serialized payload
), ),
PluginSubscribedToEvents(PluginId, ClientId, HashSet<EventType>), PluginSubscribedToEvents(PluginId, ClientId, HashSet<EventType>),
PermissionRequestResult(
PluginId,
Option<ClientId>,
Vec<PermissionType>,
PermissionStatus,
Option<PathBuf>,
),
Exit, Exit,
} }
@ -105,6 +112,9 @@ impl From<&PluginInstruction> for PluginContext {
PluginInstruction::PluginSubscribedToEvents(..) => { PluginInstruction::PluginSubscribedToEvents(..) => {
PluginContext::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 => { PluginInstruction::Exit => {
wasm_bridge.cleanup(); wasm_bridge.cleanup();
break; break;

View file

@ -188,6 +188,7 @@ impl<'a> PluginLoader<'a> {
display_loading_stage!(end, loading_indication, senders, plugin_id); display_loading_stage!(end, loading_indication, senders, plugin_id);
Ok(()) Ok(())
} }
pub fn add_client( pub fn add_client(
client_id: ClientId, client_id: ClientId,
plugin_dir: PathBuf, plugin_dir: PathBuf,
@ -613,6 +614,19 @@ impl<'a> PluginLoader<'a> {
} }
start_function.call(&[]).with_context(err_context)?; 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 let protobuf_plugin_configuration: ProtobufPluginConfiguration = self
.plugin .plugin
.userspace_configuration .userspace_configuration
@ -640,18 +654,6 @@ impl<'a> PluginLoader<'a> {
self.senders, self.senders,
self.plugin_id 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!( display_loading_stage!(
indicate_writing_plugin_to_cache_success, indicate_writing_plugin_to_cache_success,
self.loading_indication, self.loading_indication,
@ -764,13 +766,13 @@ impl<'a> PluginLoader<'a> {
}) })
.with_context(err_context)?; .with_context(err_context)?;
let wasi = wasi_env.import_object(&module).with_context(err_context)?; let wasi = wasi_env.import_object(&module).with_context(err_context)?;
let mut mut_plugin = self.plugin.clone(); let mut mut_plugin = self.plugin.clone();
mut_plugin.set_tab_index(self.tab_index); mut_plugin.set_tab_index(self.tab_index);
let plugin_env = PluginEnv { let plugin_env = PluginEnv {
plugin_id: self.plugin_id, plugin_id: self.plugin_id,
client_id: self.client_id, client_id: self.client_id,
plugin: mut_plugin, plugin: mut_plugin,
permissions: Arc::new(Mutex::new(None)),
senders: self.senders.clone(), senders: self.senders.clone(),
wasi_env, wasi_env,
plugin_own_data_dir: self.plugin_own_data_dir.clone(), plugin_own_data_dir: self.plugin_own_data_dir.clone(),

View file

@ -11,7 +11,6 @@ use wasmer_wasi::WasiEnv;
use crate::{thread_bus::ThreadSenders, ClientId}; use crate::{thread_bus::ThreadSenders, ClientId};
use zellij_utils::async_channel::Sender; use zellij_utils::async_channel::Sender;
use zellij_utils::errors::prelude::*;
use zellij_utils::{ use zellij_utils::{
data::EventType, data::EventType,
data::PluginCapabilities, data::PluginCapabilities,
@ -20,6 +19,7 @@ use zellij_utils::{
input::plugins::PluginConfig, input::plugins::PluginConfig,
ipc::ClientAttributes, 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 // 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 // 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 struct PluginEnv {
pub plugin_id: PluginId, pub plugin_id: PluginId,
pub plugin: PluginConfig, pub plugin: PluginConfig,
pub permissions: Arc<Mutex<Option<HashSet<PermissionType>>>>,
pub senders: ThreadSenders, pub senders: ThreadSenders,
pub wasi_env: WasiEnv, pub wasi_env: WasiEnv,
pub tab_index: usize, pub tab_index: usize,
@ -215,6 +216,10 @@ impl PluginEnv {
self.plugin_id self.plugin_id
) )
} }
pub fn set_permissions(&mut self, permissions: HashSet<PermissionType>) {
self.permissions.lock().unwrap().replace(permissions);
}
} }
#[derive(Eq, PartialEq, Hash)] #[derive(Eq, PartialEq, Hash)]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,7 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
expression: "format!(\"{:#?}\", permissions)"
---
Some(
[],
)

View file

@ -0,0 +1,15 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 4864
expression: "format!(\"{:#?}\", permissions)"
---
Some(
[
ReadApplicationState,
ChangeApplicationState,
OpenFiles,
RunCommands,
OpenTerminalsOrPlugins,
WriteToStdin,
],
)

View file

@ -0,0 +1,17 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 4767
expression: "format!(\"{:#?}\", new_tab_event)"
---
Some(
[
ChangeApplicationState,
ReadApplicationState,
ReadApplicationState,
ChangeApplicationState,
OpenFiles,
RunCommands,
OpenTerminalsOrPlugins,
WriteToStdin,
],
)

View file

@ -0,0 +1,6 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 903
expression: "format!(\"{:#?}\", switch_to_mode_event)"
---
None

View file

@ -13,6 +13,8 @@ use std::{
}; };
use wasmer::{Instance, Module, Store, Value}; use wasmer::{Instance, Module, Store, Value};
use zellij_utils::async_std::task::{self, JoinHandle}; use zellij_utils::async_std::task::{self, JoinHandle};
use zellij_utils::data::{PermissionStatus, PermissionType};
use zellij_utils::input::permission::PermissionCache;
use zellij_utils::notify_debouncer_full::{notify::RecommendedWatcher, Debouncer, FileIdMap}; use zellij_utils::notify_debouncer_full::{notify::RecommendedWatcher, Debouncer, FileIdMap};
use zellij_utils::plugin_api::event::ProtobufEvent; use zellij_utils::plugin_api::event::ProtobufEvent;
@ -706,6 +708,44 @@ impl WasmBridge {
}; };
} }
} }
pub fn cache_plugin_permissions(
&mut self,
plugin_id: PluginId,
client_id: Option<ClientId>,
permissions: Vec<PermissionType>,
status: PermissionStatus,
cache_path: Option<PathBuf>,
) -> Result<()> {
if let Some(running_plugin) = self
.plugin_map
.lock()
.unwrap()
.get_running_plugin(plugin_id, client_id)
{
let err_context = || format!("Failed to write plugin permission {plugin_id}");
let mut running_plugin = running_plugin.lock().unwrap();
let permissions = if status == PermissionStatus::Granted {
permissions
} else {
vec![]
};
running_plugin
.plugin_env
.set_permissions(HashSet::from_iter(permissions.clone()));
let mut permission_cache = PermissionCache::from_path_or_default(cache_path);
permission_cache.cache(
running_plugin.plugin_env.plugin.location.to_string(),
permissions,
);
permission_cache.write_to_file().with_context(err_context)?;
}
Ok(())
}
} }
fn handle_plugin_successful_loading(senders: &ThreadSenders, plugin_id: PluginId) { fn handle_plugin_successful_loading(senders: &ThreadSenders, plugin_id: PluginId) {
@ -728,6 +768,35 @@ fn handle_plugin_loading_failure(
)); ));
} }
// TODO: move to permissions?
fn check_event_permission(
plugin_env: &PluginEnv,
event: &Event,
) -> (PermissionStatus, Option<PermissionType>) {
if plugin_env.plugin.is_builtin() {
// built-in plugins can do all the things because they're part of the application and
// there's no use to deny them anything
return (PermissionStatus::Granted, None);
}
let permission = match event {
Event::ModeUpdate(..)
| Event::TabUpdate(..)
| Event::PaneUpdate(..)
| Event::CopyToClipboard(..)
| Event::SystemClipboardFailure
| Event::InputReceived => PermissionType::ReadApplicationState,
_ => return (PermissionStatus::Granted, None),
};
if let Some(permissions) = plugin_env.permissions.lock().unwrap().as_ref() {
if permissions.contains(&permission) {
return (PermissionStatus::Granted, None);
}
}
(PermissionStatus::Denied, Some(permission))
}
pub fn apply_event_to_plugin( pub fn apply_event_to_plugin(
plugin_id: PluginId, plugin_id: PluginId,
client_id: ClientId, client_id: ClientId,
@ -739,35 +808,48 @@ pub fn apply_event_to_plugin(
plugin_bytes: &mut Vec<(PluginId, ClientId, Vec<u8>)>, plugin_bytes: &mut Vec<(PluginId, ClientId, Vec<u8>)>,
) -> Result<()> { ) -> Result<()> {
let err_context = || format!("Failed to apply event to plugin {plugin_id}"); let err_context = || format!("Failed to apply event to plugin {plugin_id}");
let protobuf_event: ProtobufEvent = event match check_event_permission(plugin_env, event) {
.clone() (PermissionStatus::Granted, _) => {
.try_into() let protobuf_event: ProtobufEvent = event
.map_err(|e| anyhow!("Failed to convert to protobuf: {:?}", e))?; .clone()
let update = instance .try_into()
.exports .map_err(|e| anyhow!("Failed to convert to protobuf: {:?}", e))?;
.get_function("update") let update = instance
.with_context(err_context)?; .exports
wasi_write_object(&plugin_env.wasi_env, &protobuf_event.encode_to_vec()) .get_function("update")
.with_context(err_context)?; .with_context(err_context)?;
let update_return = update.call(&[]).with_context(err_context)?; wasi_write_object(&plugin_env.wasi_env, &protobuf_event.encode_to_vec())
let should_render = match update_return.get(0) { .with_context(err_context)?;
Some(Value::I32(n)) => *n == 1, let update_return = update.call(&[]).with_context(err_context)?;
_ => false, let should_render = match update_return.get(0) {
}; Some(Value::I32(n)) => *n == 1,
_ => false,
if rows > 0 && columns > 0 && should_render { };
let rendered_bytes = instance if rows > 0 && columns > 0 && should_render {
.exports let rendered_bytes = instance
.get_function("render") .exports
.map_err(anyError::new) .get_function("render")
.and_then(|render| {
render
.call(&[Value::I32(rows as i32), Value::I32(columns as i32)])
.map_err(anyError::new) .map_err(anyError::new)
}) .and_then(|render| {
.and_then(|_| wasi_read_string(&plugin_env.wasi_env)) render
.with_context(err_context)?; .call(&[Value::I32(rows as i32), Value::I32(columns as i32)])
plugin_bytes.push((plugin_id, client_id, rendered_bytes.as_bytes().to_vec())); .map_err(anyError::new)
})
.and_then(|_| wasi_read_string(&plugin_env.wasi_env))
.with_context(err_context)?;
plugin_bytes.push((plugin_id, client_id, rendered_bytes.as_bytes().to_vec()));
}
},
(PermissionStatus::Denied, permission) => {
log::error!(
"PluginId '{}' permission '{}' is not allowed - Event '{:?}' denied",
plugin_id,
permission
.map(|p| p.to_string())
.unwrap_or("UNKNOWN".to_owned()),
EventType::from_str(&event.to_string()).with_context(err_context)?
);
},
} }
Ok(()) Ok(())
} }

View file

@ -8,12 +8,15 @@ use std::{
collections::{BTreeMap, HashSet}, collections::{BTreeMap, HashSet},
path::PathBuf, path::PathBuf,
process, process,
str::FromStr,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
thread, thread,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use wasmer::{imports, Function, ImportObject, Store, WasmerEnv}; use wasmer::{imports, Function, ImportObject, Store, WasmerEnv};
use wasmer_wasi::WasiEnv; use wasmer_wasi::WasiEnv;
use zellij_utils::data::{CommandType, PermissionStatus, PermissionType, PluginPermission};
use zellij_utils::input::permission::PermissionCache;
use url::Url; use url::Url;
@ -86,118 +89,134 @@ impl ForeignFunctionEnv {
} }
fn host_run_plugin_command(env: &ForeignFunctionEnv) { fn host_run_plugin_command(env: &ForeignFunctionEnv) {
let err_context = || format!("failed to run plugin command {}", env.plugin_env.name());
wasi_read_bytes(&env.plugin_env.wasi_env) wasi_read_bytes(&env.plugin_env.wasi_env)
.and_then(|bytes| { .and_then(|bytes| {
let command: ProtobufPluginCommand = ProtobufPluginCommand::decode(bytes.as_slice())?; let command: ProtobufPluginCommand = ProtobufPluginCommand::decode(bytes.as_slice())?;
let command: PluginCommand = command let command: PluginCommand = command
.try_into() .try_into()
.map_err(|e| anyhow!("failed to convert serialized command: {}", e))?; .map_err(|e| anyhow!("failed to convert serialized command: {}", e))?;
match command { match check_command_permission(&env.plugin_env, &command) {
PluginCommand::Subscribe(event_list) => subscribe(env, event_list)?, (PermissionStatus::Granted, _) => match command {
PluginCommand::Unsubscribe(event_list) => unsubscribe(env, event_list)?, PluginCommand::Subscribe(event_list) => subscribe(env, event_list)?,
PluginCommand::SetSelectable(selectable) => set_selectable(env, selectable), PluginCommand::Unsubscribe(event_list) => unsubscribe(env, event_list)?,
PluginCommand::GetPluginIds => get_plugin_ids(env), PluginCommand::SetSelectable(selectable) => set_selectable(env, selectable),
PluginCommand::GetZellijVersion => get_zellij_version(env), PluginCommand::GetPluginIds => get_plugin_ids(env),
PluginCommand::OpenFile(file_to_open) => open_file(env, file_to_open), PluginCommand::GetZellijVersion => get_zellij_version(env),
PluginCommand::OpenFileFloating(file_to_open) => { PluginCommand::OpenFile(file_to_open) => open_file(env, file_to_open),
open_file_floating(env, file_to_open) PluginCommand::OpenFileFloating(file_to_open) => {
open_file_floating(env, file_to_open)
},
PluginCommand::OpenTerminal(cwd) => open_terminal(env, cwd.path.try_into()?),
PluginCommand::OpenTerminalFloating(cwd) => {
open_terminal_floating(env, cwd.path.try_into()?)
},
PluginCommand::OpenCommandPane(command_to_run) => {
open_command_pane(env, command_to_run)
},
PluginCommand::OpenCommandPaneFloating(command_to_run) => {
open_command_pane_floating(env, command_to_run)
},
PluginCommand::SwitchTabTo(tab_index) => switch_tab_to(env, tab_index),
PluginCommand::SetTimeout(seconds) => set_timeout(env, seconds),
PluginCommand::ExecCmd(command_line) => exec_cmd(env, command_line),
PluginCommand::PostMessageTo(plugin_message) => {
post_message_to(env, plugin_message)?
},
PluginCommand::PostMessageToPlugin(plugin_message) => {
post_message_to_plugin(env, plugin_message)?
},
PluginCommand::HideSelf => hide_self(env)?,
PluginCommand::ShowSelf(should_float_if_hidden) => {
show_self(env, should_float_if_hidden)
},
PluginCommand::SwitchToMode(input_mode) => {
switch_to_mode(env, input_mode.try_into()?)
},
PluginCommand::NewTabsWithLayout(raw_layout) => {
new_tabs_with_layout(env, &raw_layout)?
},
PluginCommand::NewTab => new_tab(env),
PluginCommand::GoToNextTab => go_to_next_tab(env),
PluginCommand::GoToPreviousTab => go_to_previous_tab(env),
PluginCommand::Resize(resize_payload) => resize(env, resize_payload),
PluginCommand::ResizeWithDirection(resize_strategy) => {
resize_with_direction(env, resize_strategy)
},
PluginCommand::FocusNextPane => focus_next_pane(env),
PluginCommand::FocusPreviousPane => focus_previous_pane(env),
PluginCommand::MoveFocus(direction) => move_focus(env, direction),
PluginCommand::MoveFocusOrTab(direction) => move_focus_or_tab(env, direction),
PluginCommand::Detach => detach(env),
PluginCommand::EditScrollback => edit_scrollback(env),
PluginCommand::Write(bytes) => write(env, bytes),
PluginCommand::WriteChars(chars) => write_chars(env, chars),
PluginCommand::ToggleTab => toggle_tab(env),
PluginCommand::MovePane => move_pane(env),
PluginCommand::MovePaneWithDirection(direction) => {
move_pane_with_direction(env, direction)
},
PluginCommand::ClearScreen => clear_screen(env),
PluginCommand::ScrollUp => scroll_up(env),
PluginCommand::ScrollDown => scroll_down(env),
PluginCommand::ScrollToTop => scroll_to_top(env),
PluginCommand::ScrollToBottom => scroll_to_bottom(env),
PluginCommand::PageScrollUp => page_scroll_up(env),
PluginCommand::PageScrollDown => page_scroll_down(env),
PluginCommand::ToggleFocusFullscreen => toggle_focus_fullscreen(env),
PluginCommand::TogglePaneFrames => toggle_pane_frames(env),
PluginCommand::TogglePaneEmbedOrEject => toggle_pane_embed_or_eject(env),
PluginCommand::UndoRenamePane => undo_rename_pane(env),
PluginCommand::CloseFocus => close_focus(env),
PluginCommand::ToggleActiveTabSync => toggle_active_tab_sync(env),
PluginCommand::CloseFocusedTab => close_focused_tab(env),
PluginCommand::UndoRenameTab => undo_rename_tab(env),
PluginCommand::QuitZellij => quit_zellij(env),
PluginCommand::PreviousSwapLayout => previous_swap_layout(env),
PluginCommand::NextSwapLayout => next_swap_layout(env),
PluginCommand::GoToTabName(tab_name) => go_to_tab_name(env, tab_name),
PluginCommand::FocusOrCreateTab(tab_name) => focus_or_create_tab(env, tab_name),
PluginCommand::GoToTab(tab_index) => go_to_tab(env, tab_index),
PluginCommand::StartOrReloadPlugin(plugin_url) => {
start_or_reload_plugin(env, &plugin_url)?
},
PluginCommand::CloseTerminalPane(terminal_pane_id) => {
close_terminal_pane(env, terminal_pane_id)
},
PluginCommand::ClosePluginPane(plugin_pane_id) => {
close_plugin_pane(env, plugin_pane_id)
},
PluginCommand::FocusTerminalPane(terminal_pane_id, should_float_if_hidden) => {
focus_terminal_pane(env, terminal_pane_id, should_float_if_hidden)
},
PluginCommand::FocusPluginPane(plugin_pane_id, should_float_if_hidden) => {
focus_plugin_pane(env, plugin_pane_id, should_float_if_hidden)
},
PluginCommand::RenameTerminalPane(terminal_pane_id, new_name) => {
rename_terminal_pane(env, terminal_pane_id, &new_name)
},
PluginCommand::RenamePluginPane(plugin_pane_id, new_name) => {
rename_plugin_pane(env, plugin_pane_id, &new_name)
},
PluginCommand::RenameTab(tab_index, new_name) => {
rename_tab(env, tab_index, &new_name)
},
PluginCommand::ReportPanic(crash_payload) => report_panic(env, &crash_payload),
PluginCommand::RequestPluginPermissions(permissions) => {
request_permission(env, permissions)?
},
}, },
PluginCommand::OpenTerminal(cwd) => open_terminal(env, cwd.path.try_into()?), (PermissionStatus::Denied, permission) => {
PluginCommand::OpenTerminalFloating(cwd) => { log::error!(
open_terminal_floating(env, cwd.path.try_into()?) "Plugin '{}' permission '{}' denied - Command '{:?}' denied",
env.plugin_env.name(),
permission
.map(|p| p.to_string())
.unwrap_or("UNKNOWN".to_owned()),
CommandType::from_str(&command.to_string()).with_context(err_context)?
);
}, },
PluginCommand::OpenCommandPane(command_to_run) => { };
open_command_pane(env, command_to_run)
},
PluginCommand::OpenCommandPaneFloating(command_to_run) => {
open_command_pane_floating(env, command_to_run)
},
PluginCommand::SwitchTabTo(tab_index) => switch_tab_to(env, tab_index),
PluginCommand::SetTimeout(seconds) => set_timeout(env, seconds),
PluginCommand::ExecCmd(command_line) => exec_cmd(env, command_line),
PluginCommand::PostMessageTo(plugin_message) => {
post_message_to(env, plugin_message)?
},
PluginCommand::PostMessageToPlugin(plugin_message) => {
post_message_to_plugin(env, plugin_message)?
},
PluginCommand::HideSelf => hide_self(env)?,
PluginCommand::ShowSelf(should_float_if_hidden) => {
show_self(env, should_float_if_hidden)
},
PluginCommand::SwitchToMode(input_mode) => {
switch_to_mode(env, input_mode.try_into()?)
},
PluginCommand::NewTabsWithLayout(raw_layout) => {
new_tabs_with_layout(env, &raw_layout)?
},
PluginCommand::NewTab => new_tab(env),
PluginCommand::GoToNextTab => go_to_next_tab(env),
PluginCommand::GoToPreviousTab => go_to_previous_tab(env),
PluginCommand::Resize(resize_payload) => resize(env, resize_payload),
PluginCommand::ResizeWithDirection(resize_strategy) => {
resize_with_direction(env, resize_strategy)
},
PluginCommand::FocusNextPane => focus_next_pane(env),
PluginCommand::FocusPreviousPane => focus_previous_pane(env),
PluginCommand::MoveFocus(direction) => move_focus(env, direction),
PluginCommand::MoveFocusOrTab(direction) => move_focus_or_tab(env, direction),
PluginCommand::Detach => detach(env),
PluginCommand::EditScrollback => edit_scrollback(env),
PluginCommand::Write(bytes) => write(env, bytes),
PluginCommand::WriteChars(chars) => write_chars(env, chars),
PluginCommand::ToggleTab => toggle_tab(env),
PluginCommand::MovePane => move_pane(env),
PluginCommand::MovePaneWithDirection(direction) => {
move_pane_with_direction(env, direction)
},
PluginCommand::ClearScreen => clear_screen(env),
PluginCommand::ScrollUp => scroll_up(env),
PluginCommand::ScrollDown => scroll_down(env),
PluginCommand::ScrollToTop => scroll_to_top(env),
PluginCommand::ScrollToBottom => scroll_to_bottom(env),
PluginCommand::PageScrollUp => page_scroll_up(env),
PluginCommand::PageScrollDown => page_scroll_down(env),
PluginCommand::ToggleFocusFullscreen => toggle_focus_fullscreen(env),
PluginCommand::TogglePaneFrames => toggle_pane_frames(env),
PluginCommand::TogglePaneEmbedOrEject => toggle_pane_embed_or_eject(env),
PluginCommand::UndoRenamePane => undo_rename_pane(env),
PluginCommand::CloseFocus => close_focus(env),
PluginCommand::ToggleActiveTabSync => toggle_active_tab_sync(env),
PluginCommand::CloseFocusedTab => close_focused_tab(env),
PluginCommand::UndoRenameTab => undo_rename_tab(env),
PluginCommand::QuitZellij => quit_zellij(env),
PluginCommand::PreviousSwapLayout => previous_swap_layout(env),
PluginCommand::NextSwapLayout => next_swap_layout(env),
PluginCommand::GoToTabName(tab_name) => go_to_tab_name(env, tab_name),
PluginCommand::FocusOrCreateTab(tab_name) => focus_or_create_tab(env, tab_name),
PluginCommand::GoToTab(tab_index) => go_to_tab(env, tab_index),
PluginCommand::StartOrReloadPlugin(plugin_url) => {
start_or_reload_plugin(env, &plugin_url)?
},
PluginCommand::CloseTerminalPane(terminal_pane_id) => {
close_terminal_pane(env, terminal_pane_id)
},
PluginCommand::ClosePluginPane(plugin_pane_id) => {
close_plugin_pane(env, plugin_pane_id)
},
PluginCommand::FocusTerminalPane(terminal_pane_id, should_float_if_hidden) => {
focus_terminal_pane(env, terminal_pane_id, should_float_if_hidden)
},
PluginCommand::FocusPluginPane(plugin_pane_id, should_float_if_hidden) => {
focus_plugin_pane(env, plugin_pane_id, should_float_if_hidden)
},
PluginCommand::RenameTerminalPane(terminal_pane_id, new_name) => {
rename_terminal_pane(env, terminal_pane_id, &new_name)
},
PluginCommand::RenamePluginPane(plugin_pane_id, new_name) => {
rename_plugin_pane(env, plugin_pane_id, &new_name)
},
PluginCommand::RenameTab(tab_index, new_name) => {
rename_tab(env, tab_index, &new_name)
},
PluginCommand::ReportPanic(crash_payload) => report_panic(env, &crash_payload),
}
Ok(()) Ok(())
}) })
.with_context(|| format!("failed to run plugin command {}", env.plugin_env.name())) .with_context(|| format!("failed to run plugin command {}", env.plugin_env.name()))
@ -255,6 +274,30 @@ fn set_selectable(env: &ForeignFunctionEnv, selectable: bool) {
} }
} }
fn request_permission(env: &ForeignFunctionEnv, permissions: Vec<PermissionType>) -> Result<()> {
if PermissionCache::from_path_or_default(None)
.check_permissions(env.plugin_env.plugin.location.to_string(), &permissions)
{
return env
.plugin_env
.senders
.send_to_plugin(PluginInstruction::PermissionRequestResult(
env.plugin_env.plugin_id,
Some(env.plugin_env.client_id),
permissions.to_vec(),
PermissionStatus::Granted,
None,
));
}
env.plugin_env
.senders
.send_to_screen(ScreenInstruction::RequestPluginPermissions(
env.plugin_env.plugin_id,
PluginPermission::new(env.plugin_env.plugin.location.to_string(), permissions),
))
}
fn get_plugin_ids(env: &ForeignFunctionEnv) { fn get_plugin_ids(env: &ForeignFunctionEnv) {
let ids = PluginIds { let ids = PluginIds {
plugin_id: env.plugin_env.plugin_id, plugin_id: env.plugin_env.plugin_id,
@ -1001,3 +1044,81 @@ pub fn wasi_read_bytes(wasi_env: &WasiEnv) -> Result<Vec<u8>> {
.and_then(|string| serde_json::from_str(&string).map_err(anyError::new)) .and_then(|string| serde_json::from_str(&string).map_err(anyError::new))
.with_context(|| format!("failed to deserialize object from WASI env '{wasi_env:?}'")) .with_context(|| format!("failed to deserialize object from WASI env '{wasi_env:?}'"))
} }
// TODO: move to permissions?
fn check_command_permission(
plugin_env: &PluginEnv,
command: &PluginCommand,
) -> (PermissionStatus, Option<PermissionType>) {
if plugin_env.plugin.is_builtin() {
// built-in plugins can do all the things because they're part of the application and
// there's no use to deny them anything
return (PermissionStatus::Granted, None);
}
let permission = match command {
PluginCommand::OpenFile(..) | PluginCommand::OpenFileFloating(..) => {
PermissionType::OpenFiles
},
PluginCommand::OpenTerminal(..)
| PluginCommand::StartOrReloadPlugin(..)
| PluginCommand::OpenTerminalFloating(..) => PermissionType::OpenTerminalsOrPlugins,
PluginCommand::OpenCommandPane(..)
| PluginCommand::OpenCommandPaneFloating(..)
| PluginCommand::ExecCmd(..) => PermissionType::RunCommands,
PluginCommand::Write(..) | PluginCommand::WriteChars(..) => PermissionType::WriteToStdin,
PluginCommand::SwitchTabTo(..)
| PluginCommand::SwitchToMode(..)
| PluginCommand::NewTabsWithLayout(..)
| PluginCommand::NewTab
| PluginCommand::GoToNextTab
| PluginCommand::GoToPreviousTab
| PluginCommand::Resize(..)
| PluginCommand::ResizeWithDirection(..)
| PluginCommand::FocusNextPane
| PluginCommand::MoveFocus(..)
| PluginCommand::MoveFocusOrTab(..)
| PluginCommand::Detach
| PluginCommand::EditScrollback
| PluginCommand::ToggleTab
| PluginCommand::MovePane
| PluginCommand::MovePaneWithDirection(..)
| PluginCommand::ClearScreen
| PluginCommand::ScrollUp
| PluginCommand::ScrollDown
| PluginCommand::ScrollToTop
| PluginCommand::ScrollToBottom
| PluginCommand::PageScrollUp
| PluginCommand::PageScrollDown
| PluginCommand::ToggleFocusFullscreen
| PluginCommand::TogglePaneFrames
| PluginCommand::TogglePaneEmbedOrEject
| PluginCommand::UndoRenamePane
| PluginCommand::CloseFocus
| PluginCommand::ToggleActiveTabSync
| PluginCommand::CloseFocusedTab
| PluginCommand::UndoRenameTab
| PluginCommand::QuitZellij
| PluginCommand::PreviousSwapLayout
| PluginCommand::NextSwapLayout
| PluginCommand::GoToTabName(..)
| PluginCommand::FocusOrCreateTab(..)
| PluginCommand::GoToTab(..)
| PluginCommand::CloseTerminalPane(..)
| PluginCommand::ClosePluginPane(..)
| PluginCommand::FocusTerminalPane(..)
| PluginCommand::FocusPluginPane(..)
| PluginCommand::RenameTerminalPane(..)
| PluginCommand::RenamePluginPane(..)
| PluginCommand::RenameTab(..) => PermissionType::ChangeApplicationState,
_ => return (PermissionStatus::Granted, None),
};
log::info!("plugin permissions: {:?}", plugin_env.permissions);
if let Some(permissions) = plugin_env.permissions.lock().unwrap().as_ref() {
if permissions.contains(&permission) {
return (PermissionStatus::Granted, None);
}
}
(PermissionStatus::Denied, Some(permission))
}

View file

@ -6,7 +6,7 @@ use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
use std::str; use std::str;
use zellij_utils::data::{Direction, PaneManifest, Resize, ResizeStrategy}; use zellij_utils::data::{Direction, PaneManifest, PluginPermission, Resize, ResizeStrategy};
use zellij_utils::errors::prelude::*; use zellij_utils::errors::prelude::*;
use zellij_utils::input::command::RunCommand; use zellij_utils::input::command::RunCommand;
use zellij_utils::input::options::Clipboard; use zellij_utils::input::options::Clipboard;
@ -280,6 +280,10 @@ pub enum ScreenInstruction {
FocusPaneWithId(PaneId, bool, ClientId), // bool is should_float FocusPaneWithId(PaneId, bool, ClientId), // bool is should_float
RenamePane(PaneId, Vec<u8>), RenamePane(PaneId, Vec<u8>),
RenameTab(usize, Vec<u8>), RenameTab(usize, Vec<u8>),
RequestPluginPermissions(
u32, // u32 - plugin_id
PluginPermission,
),
BreakPane(Box<Layout>, Option<TerminalAction>, ClientId), BreakPane(Box<Layout>, Option<TerminalAction>, ClientId),
BreakPaneRight(ClientId), BreakPaneRight(ClientId),
BreakPaneLeft(ClientId), BreakPaneLeft(ClientId),
@ -450,6 +454,9 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::FocusPaneWithId(..) => ScreenContext::FocusPaneWithId, ScreenInstruction::FocusPaneWithId(..) => ScreenContext::FocusPaneWithId,
ScreenInstruction::RenamePane(..) => ScreenContext::RenamePane, ScreenInstruction::RenamePane(..) => ScreenContext::RenamePane,
ScreenInstruction::RenameTab(..) => ScreenContext::RenameTab, ScreenInstruction::RenameTab(..) => ScreenContext::RenameTab,
ScreenInstruction::RequestPluginPermissions(..) => {
ScreenContext::RequestPluginPermissions
},
ScreenInstruction::BreakPane(..) => ScreenContext::BreakPane, ScreenInstruction::BreakPane(..) => ScreenContext::BreakPane,
ScreenInstruction::BreakPaneRight(..) => ScreenContext::BreakPaneRight, ScreenInstruction::BreakPaneRight(..) => ScreenContext::BreakPaneRight,
ScreenInstruction::BreakPaneLeft(..) => ScreenContext::BreakPaneLeft, ScreenInstruction::BreakPaneLeft(..) => ScreenContext::BreakPaneLeft,
@ -2958,6 +2965,24 @@ pub(crate) fn screen_thread_main(
} }
screen.report_tab_state()?; screen.report_tab_state()?;
}, },
ScreenInstruction::RequestPluginPermissions(plugin_id, plugin_permission) => {
let all_tabs = screen.get_tabs_mut();
let found = all_tabs.values_mut().any(|tab| {
if tab.has_plugin(plugin_id) {
tab.request_plugin_permissions(plugin_id, Some(plugin_permission.clone()));
true
} else {
false
}
});
if !found {
log::error!(
"PluginId '{}' not found - cannot request permissions",
plugin_id
);
}
},
ScreenInstruction::BreakPane(default_layout, default_shell, client_id) => { ScreenInstruction::BreakPane(default_layout, default_shell, client_id) => {
screen.break_pane(default_shell, default_layout, client_id)?; screen.break_pane(default_shell, default_layout, client_id)?;
}, },

View file

@ -9,7 +9,9 @@ mod swap_layouts;
use copy_command::CopyCommand; use copy_command::CopyCommand;
use std::env::temp_dir; use std::env::temp_dir;
use uuid::Uuid; use uuid::Uuid;
use zellij_utils::data::{Direction, PaneInfo, ResizeStrategy}; use zellij_utils::data::{
Direction, PaneInfo, PermissionStatus, PermissionType, PluginPermission, ResizeStrategy,
};
use zellij_utils::errors::prelude::*; use zellij_utils::errors::prelude::*;
use zellij_utils::input::command::RunCommand; use zellij_utils::input::command::RunCommand;
use zellij_utils::position::{Column, Line}; use zellij_utils::position::{Column, Line};
@ -220,6 +222,7 @@ pub trait Pane {
fn set_should_render_boundaries(&mut self, _should_render: bool) {} fn set_should_render_boundaries(&mut self, _should_render: bool) {}
fn selectable(&self) -> bool; fn selectable(&self) -> bool;
fn set_selectable(&mut self, selectable: bool); fn set_selectable(&mut self, selectable: bool);
fn request_permissions_from_user(&mut self, _permissions: Option<PluginPermission>) {}
fn render( fn render(
&mut self, &mut self,
client_id: Option<ClientId>, client_id: Option<ClientId>,
@ -473,6 +476,7 @@ pub trait Pane {
pub enum AdjustedInput { pub enum AdjustedInput {
WriteBytesToTerminal(Vec<u8>), WriteBytesToTerminal(Vec<u8>),
ReRunCommandInThisPane(RunCommand), ReRunCommandInThisPane(RunCommand),
PermissionRequestResult(Vec<PermissionType>, PermissionStatus),
CloseThisPane, CloseThisPane,
} }
pub fn get_next_terminal_position( pub fn get_next_terminal_position(
@ -1560,17 +1564,35 @@ impl Tab {
self.close_pane(PaneId::Terminal(active_terminal_id), false, None); self.close_pane(PaneId::Terminal(active_terminal_id), false, None);
should_update_ui = true; should_update_ui = true;
}, },
Some(_) => {},
None => {}, None => {},
} }
}, },
PaneId::Plugin(pid) => { PaneId::Plugin(pid) => match active_terminal.adjust_input_to_terminal(input_bytes) {
let mut plugin_updates = vec![]; Some(AdjustedInput::WriteBytesToTerminal(adjusted_input)) => {
for key in parse_keys(&input_bytes) { let mut plugin_updates = vec![];
plugin_updates.push((Some(pid), client_id, Event::Key(key))); for key in parse_keys(&adjusted_input) {
} plugin_updates.push((Some(pid), client_id, Event::Key(key)));
self.senders }
.send_to_plugin(PluginInstruction::Update(plugin_updates)) self.senders
.with_context(err_context)?; .send_to_plugin(PluginInstruction::Update(plugin_updates))
.with_context(err_context)?;
},
Some(AdjustedInput::PermissionRequestResult(permissions, status)) => {
self.request_plugin_permissions(pid, None);
self.senders
.send_to_plugin(PluginInstruction::PermissionRequestResult(
pid,
client_id,
permissions,
status,
None,
))
.with_context(err_context)?;
should_update_ui = true;
},
Some(_) => {},
None => {},
}, },
} }
Ok(should_update_ui) Ok(should_update_ui)
@ -3437,6 +3459,20 @@ impl Tab {
} }
Ok(()) Ok(())
} }
pub fn request_plugin_permissions(&mut self, pid: u32, permissions: Option<PluginPermission>) {
if let Some(plugin_pane) = self
.tiled_panes
.get_pane_mut(PaneId::Plugin(pid))
.or_else(|| self.floating_panes.get_pane_mut(PaneId::Plugin(pid)))
.or_else(|| {
self.suppressed_panes
.values_mut()
.find(|s_p| s_p.pid() == PaneId::Plugin(pid))
})
{
plugin_pane.request_permissions_from_user(permissions);
}
}
} }
pub fn pane_info_for_pane(pane_id: &PaneId, pane: &Box<dyn Pane>) -> PaneInfo { pub fn pane_info_for_pane(pane_id: &PaneId, pane: &Box<dyn Pane>) -> PaneInfo {

View file

@ -107,6 +107,7 @@ macro_rules! register_plugin {
fn load() { fn load() {
STATE.with(|state| { STATE.with(|state| {
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::convert::TryInto; use std::convert::TryInto;
use zellij_tile::shim::plugin_api::action::ProtobufPluginConfiguration; use zellij_tile::shim::plugin_api::action::ProtobufPluginConfiguration;
use zellij_tile::shim::prost::Message; use zellij_tile::shim::prost::Message;

View file

@ -39,6 +39,13 @@ pub fn set_selectable(selectable: bool) {
unsafe { host_run_plugin_command() }; unsafe { host_run_plugin_command() };
} }
pub fn request_permission(permissions: &[PermissionType]) {
let plugin_command = PluginCommand::RequestPluginPermissions(permissions.into());
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}
// Query Functions // Query Functions
/// Returns the unique Zellij pane ID for the plugin as well as the Zellij process id. /// Returns the unique Zellij pane ID for the plugin as well as the Zellij process id.
pub fn get_plugin_ids() -> PluginIds { pub fn get_plugin_ids() -> PluginIds {

View file

@ -36,6 +36,8 @@ lazy_static! {
.cache_dir() .cache_dir()
.to_path_buf() .to_path_buf()
.join(format!("{}", Uuid::new_v4())); .join(format!("{}", Uuid::new_v4()));
pub static ref ZELLIJ_PLUGIN_PERMISSIONS_CACHE: PathBuf =
ZELLIJ_CACHE_DIR.join("permissions.kdl");
} }
pub const FEATURES: &[&str] = &[ pub const FEATURES: &[&str] = &[

View file

@ -6,7 +6,7 @@ use std::collections::{HashMap, HashSet};
use std::fmt; use std::fmt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
use strum_macros::{EnumDiscriminants, EnumIter, EnumString, ToString}; use strum_macros::{Display, EnumDiscriminants, EnumIter, EnumString, ToString};
pub type ClientId = u16; // TODO: merge with crate type? pub type ClientId = u16; // TODO: merge with crate type?
@ -493,6 +493,63 @@ pub enum Event {
FileSystemUpdate(Vec<PathBuf>), FileSystemUpdate(Vec<PathBuf>),
/// A file was deleted somewhere in the Zellij CWD folder /// A file was deleted somewhere in the Zellij CWD folder
FileSystemDelete(Vec<PathBuf>), FileSystemDelete(Vec<PathBuf>),
/// A Result of plugin permission request
PermissionRequestResult(PermissionStatus),
}
#[derive(
Debug,
PartialEq,
Eq,
Hash,
Copy,
Clone,
EnumDiscriminants,
ToString,
Serialize,
Deserialize,
PartialOrd,
Ord,
)]
#[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize, Display, PartialOrd, Ord))]
#[strum_discriminants(name(PermissionType))]
#[non_exhaustive]
pub enum Permission {
ReadApplicationState,
ChangeApplicationState,
OpenFiles,
RunCommands,
OpenTerminalsOrPlugins,
WriteToStdin,
}
impl PermissionType {
pub fn display_name(&self) -> String {
match self {
PermissionType::ReadApplicationState => {
"Access Zellij state (Panes, Tabs and UI)".to_owned()
},
PermissionType::ChangeApplicationState => {
"Change Zellij state (Panes, Tabs and UI)".to_owned()
},
PermissionType::OpenFiles => "Open files (eg. for editing)".to_owned(),
PermissionType::RunCommands => "Run commands".to_owned(),
PermissionType::OpenTerminalsOrPlugins => "Start new terminals and plugins".to_owned(),
PermissionType::WriteToStdin => "Write to standard input (STDIN)".to_owned(),
}
}
}
#[derive(Debug, Clone)]
pub struct PluginPermission {
pub name: String,
pub permissions: Vec<PermissionType>,
}
impl PluginPermission {
pub fn new(name: String, permissions: Vec<PermissionType>) -> Self {
PluginPermission { name, permissions }
}
} }
/// Describes the different input modes, which change the way that keystrokes will be interpreted. /// Describes the different input modes, which change the way that keystrokes will be interpreted.
@ -811,6 +868,12 @@ pub enum CopyDestination {
System, System,
} }
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub enum PermissionStatus {
Granted,
Denied,
}
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct FileToOpen { pub struct FileToOpen {
pub path: PathBuf, pub path: PathBuf,
@ -882,7 +945,9 @@ impl PluginMessage {
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, EnumDiscriminants, ToString)]
#[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize))]
#[strum_discriminants(name(CommandType))]
pub enum PluginCommand { pub enum PluginCommand {
Subscribe(HashSet<EventType>), Subscribe(HashSet<EventType>),
Unsubscribe(HashSet<EventType>), Unsubscribe(HashSet<EventType>),
@ -950,4 +1015,5 @@ pub enum PluginCommand {
RenamePluginPane(u32, String), // plugin pane id, new name RenamePluginPane(u32, String), // plugin pane id, new name
RenameTab(u32, String), // tab index, new name RenameTab(u32, String), // tab index, new name
ReportPanic(String), // stringified panic ReportPanic(String), // stringified panic
RequestPluginPermissions(Vec<PermissionType>),
} }

View file

@ -338,6 +338,7 @@ pub enum ScreenContext {
FocusPaneWithId, FocusPaneWithId,
RenamePane, RenamePane,
RenameTab, RenameTab,
RequestPluginPermissions,
BreakPane, BreakPane,
BreakPaneRight, BreakPaneRight,
BreakPaneLeft, BreakPaneLeft,
@ -377,6 +378,7 @@ pub enum PluginContext {
PostMessageToPluginWorker, PostMessageToPluginWorker,
PostMessageToPlugin, PostMessageToPlugin,
PluginSubscribedToEvents, PluginSubscribedToEvents,
PermissionRequestResult,
} }
/// Stack call representations corresponding to the different types of [`ClientInstruction`]s. /// Stack call representations corresponding to the different types of [`ClientInstruction`]s.

View file

@ -4,6 +4,7 @@ pub mod config;
pub mod keybinds; pub mod keybinds;
pub mod layout; pub mod layout;
pub mod options; pub mod options;
pub mod permission;
pub mod plugins; pub mod plugins;
pub mod theme; pub mod theme;

View file

@ -0,0 +1,60 @@
use std::{
collections::HashMap,
fs::{self, File},
io::Write,
path::PathBuf,
};
use crate::{consts::ZELLIJ_PLUGIN_PERMISSIONS_CACHE, data::PermissionType};
pub type GrantedPermission = HashMap<String, Vec<PermissionType>>;
#[derive(Default, Debug)]
pub struct PermissionCache {
path: PathBuf,
granted: GrantedPermission,
}
impl PermissionCache {
pub fn cache(&mut self, plugin_name: String, permissions: Vec<PermissionType>) {
self.granted.insert(plugin_name, permissions);
}
pub fn get_permissions(&self, plugin_name: String) -> Option<&Vec<PermissionType>> {
self.granted.get(&plugin_name)
}
pub fn check_permissions(
&self,
plugin_name: String,
permissions: &Vec<PermissionType>,
) -> bool {
if let Some(target) = self.granted.get(&plugin_name) {
if target == permissions {
return true;
}
}
false
}
pub fn from_path_or_default(cache_path: Option<PathBuf>) -> Self {
let cache_path = cache_path.unwrap_or(ZELLIJ_PLUGIN_PERMISSIONS_CACHE.to_path_buf());
let granted = match fs::read_to_string(cache_path.clone()) {
Ok(raw_string) => PermissionCache::from_string(raw_string).unwrap_or_default(),
Err(_) => GrantedPermission::default(),
};
PermissionCache {
path: cache_path,
granted,
}
}
pub fn write_to_file(&self) -> std::io::Result<()> {
let mut f = File::create(&self.path)?;
write!(f, "{}", PermissionCache::to_string(&self.granted))?;
Ok(())
}
}

View file

@ -1,15 +1,16 @@
mod kdl_layout_parser; mod kdl_layout_parser;
use crate::data::{Direction, InputMode, Key, Palette, PaletteColor, Resize}; use crate::data::{Direction, InputMode, Key, Palette, PaletteColor, PermissionType, Resize};
use crate::envs::EnvironmentVariables; use crate::envs::EnvironmentVariables;
use crate::input::config::{Config, ConfigError, KdlError}; use crate::input::config::{Config, ConfigError, KdlError};
use crate::input::keybinds::Keybinds; use crate::input::keybinds::Keybinds;
use crate::input::layout::{Layout, PluginUserConfiguration, RunPlugin, RunPluginLocation}; use crate::input::layout::{Layout, PluginUserConfiguration, RunPlugin, RunPluginLocation};
use crate::input::options::{Clipboard, OnForceClose, Options}; use crate::input::options::{Clipboard, OnForceClose, Options};
use crate::input::permission::{GrantedPermission, PermissionCache};
use crate::input::plugins::{PluginConfig, PluginTag, PluginType, PluginsConfig}; use crate::input::plugins::{PluginConfig, PluginTag, PluginType, PluginsConfig};
use crate::input::theme::{FrameConfig, Theme, Themes, UiConfig}; use crate::input::theme::{FrameConfig, Theme, Themes, UiConfig};
use crate::setup::{find_default_config_dir, get_layout_dir}; use crate::setup::{find_default_config_dir, get_layout_dir};
use kdl_layout_parser::KdlLayoutParser; use kdl_layout_parser::KdlLayoutParser;
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap, HashSet};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use miette::NamedSource; use miette::NamedSource;
@ -1793,6 +1794,53 @@ impl Themes {
} }
} }
impl PermissionCache {
pub fn from_string(raw_string: String) -> Result<GrantedPermission, ConfigError> {
let kdl_document: KdlDocument = raw_string.parse()?;
let mut granted_permission = GrantedPermission::default();
for node in kdl_document.nodes() {
if let Some(children) = node.children() {
let key = kdl_name!(node);
let permissions: Vec<PermissionType> = children
.nodes()
.iter()
.filter_map(|p| {
let v = kdl_name!(p);
PermissionType::from_str(v).ok()
})
.collect();
granted_permission.insert(key.into(), permissions);
}
}
Ok(granted_permission)
}
pub fn to_string(granted: &GrantedPermission) -> String {
let mut kdl_doucment = KdlDocument::new();
granted.iter().for_each(|(k, v)| {
let mut node = KdlNode::new(k.as_str());
let mut children = KdlDocument::new();
let permissions: HashSet<PermissionType> = v.clone().into_iter().collect();
permissions.iter().for_each(|f| {
let n = KdlNode::new(f.to_string().as_str());
children.nodes_mut().push(n);
});
node.set_children(children);
kdl_doucment.nodes_mut().push(node);
});
kdl_doucment.fmt();
kdl_doucment.to_string()
}
}
pub fn parse_plugin_user_configuration( pub fn parse_plugin_user_configuration(
plugin_block: &KdlNode, plugin_block: &KdlNode,
) -> Result<BTreeMap<String, String>, ConfigError> { ) -> Result<BTreeMap<String, String>, ConfigError> {

View file

@ -38,6 +38,7 @@ enum EventType {
FileSystemUpdate = 13; FileSystemUpdate = 13;
/// A file was deleted somewhere in the Zellij CWD folder /// A file was deleted somewhere in the Zellij CWD folder
FileSystemDelete = 14; FileSystemDelete = 14;
PermissionRequestResult = 15;
} }
message EventNameList { message EventNameList {
@ -57,9 +58,14 @@ message Event {
bool visible_payload = 9; bool visible_payload = 9;
CustomMessagePayload custom_message_payload = 10; CustomMessagePayload custom_message_payload = 10;
FileListPayload file_list_payload = 11; FileListPayload file_list_payload = 11;
PermissionRequestResultPayload permission_request_result_payload = 12;
} }
} }
message PermissionRequestResultPayload {
bool granted = 1;
}
message FileListPayload { message FileListPayload {
repeated string paths = 1; repeated string paths = 1;
} }

View file

@ -13,9 +13,10 @@ pub use super::generated_api::api::{
style::Style as ProtobufStyle, style::Style as ProtobufStyle,
}; };
use crate::data::{ use crate::data::{
CopyDestination, Direction, Event, EventType, InputMode, Key, ModeInfo, Mouse, Palette, CopyDestination, Event, EventType, InputMode, Key, ModeInfo, Mouse, PaneInfo, PaneManifest,
PaletteColor, PaneInfo, PaneManifest, PluginCapabilities, Style, TabInfo, ThemeHue, PermissionStatus, PluginCapabilities, Style, TabInfo,
}; };
use crate::errors::prelude::*; use crate::errors::prelude::*;
use crate::input::actions::Action; use crate::input::actions::Action;
@ -160,6 +161,16 @@ impl TryFrom<ProtobufEvent> for Event {
}, },
_ => Err("Malformed payload for the file system delete Event"), _ => Err("Malformed payload for the file system delete Event"),
}, },
Some(ProtobufEventType::PermissionRequestResult) => match protobuf_event.payload {
Some(ProtobufEventPayload::PermissionRequestResultPayload(payload)) => {
if payload.granted {
Ok(Event::PermissionRequestResult(PermissionStatus::Granted))
} else {
Ok(Event::PermissionRequestResult(PermissionStatus::Denied))
}
},
_ => Err("Malformed payload for the file system delete Event"),
},
None => Err("Unknown Protobuf Event"), None => Err("Unknown Protobuf Event"),
} }
} }
@ -290,6 +301,18 @@ impl TryFrom<Event> for ProtobufEvent {
payload: Some(event::Payload::FileListPayload(file_list_payload)), payload: Some(event::Payload::FileListPayload(file_list_payload)),
}) })
}, },
Event::PermissionRequestResult(permission_status) => {
let granted = match permission_status {
PermissionStatus::Granted => true,
PermissionStatus::Denied => false,
};
Ok(ProtobufEvent {
name: ProtobufEventType::PermissionRequestResult as i32,
payload: Some(event::Payload::PermissionRequestResultPayload(
PermissionRequestResultPayload { granted },
)),
})
},
} }
} }
} }
@ -673,6 +696,7 @@ impl TryFrom<ProtobufEventType> for EventType {
ProtobufEventType::FileSystemRead => EventType::FileSystemRead, ProtobufEventType::FileSystemRead => EventType::FileSystemRead,
ProtobufEventType::FileSystemUpdate => EventType::FileSystemUpdate, ProtobufEventType::FileSystemUpdate => EventType::FileSystemUpdate,
ProtobufEventType::FileSystemDelete => EventType::FileSystemDelete, ProtobufEventType::FileSystemDelete => EventType::FileSystemDelete,
ProtobufEventType::PermissionRequestResult => EventType::PermissionRequestResult,
}) })
} }
} }
@ -696,6 +720,7 @@ impl TryFrom<EventType> for ProtobufEventType {
EventType::FileSystemRead => ProtobufEventType::FileSystemRead, EventType::FileSystemRead => ProtobufEventType::FileSystemRead,
EventType::FileSystemUpdate => ProtobufEventType::FileSystemUpdate, EventType::FileSystemUpdate => ProtobufEventType::FileSystemUpdate,
EventType::FileSystemDelete => ProtobufEventType::FileSystemDelete, EventType::FileSystemDelete => ProtobufEventType::FileSystemDelete,
EventType::PermissionRequestResult => ProtobufEventType::PermissionRequestResult,
}) })
} }
} }
@ -717,6 +742,7 @@ fn serialize_mode_update_event() {
#[test] #[test]
fn serialize_mode_update_event_with_non_default_values() { fn serialize_mode_update_event_with_non_default_values() {
use crate::data::{Direction, Palette, PaletteColor, ThemeHue};
use prost::Message; use prost::Message;
let mode_update_event = Event::ModeUpdate(ModeInfo { let mode_update_event = Event::ModeUpdate(ModeInfo {
mode: InputMode::Locked, mode: InputMode::Locked,

View file

@ -7,6 +7,7 @@ pub mod key;
pub mod message; pub mod message;
pub mod plugin_command; pub mod plugin_command;
pub mod plugin_ids; pub mod plugin_ids;
pub mod plugin_permission;
pub mod resize; pub mod resize;
pub mod style; pub mod style;
pub mod generated_api { pub mod generated_api {

View file

@ -6,6 +6,7 @@ import "command.proto";
import "message.proto"; import "message.proto";
import "input_mode.proto"; import "input_mode.proto";
import "resize.proto"; import "resize.proto";
import "plugin_permission.proto";
package api.plugin_command; package api.plugin_command;
@ -76,6 +77,7 @@ enum CommandName {
RenamePluginPane = 63; RenamePluginPane = 63;
RenameTab = 64; RenameTab = 64;
ReportCrash = 65; ReportCrash = 65;
RequestPluginPermissions = 66;
} }
message PluginCommand { message PluginCommand {
@ -117,9 +119,14 @@ message PluginCommand {
IdAndNewName rename_plugin_pane_payload = 35; IdAndNewName rename_plugin_pane_payload = 35;
IdAndNewName rename_tab_payload = 36; IdAndNewName rename_tab_payload = 36;
string report_crash_payload = 37; string report_crash_payload = 37;
RequestPluginPermissionPayload request_plugin_permission_payload = 38;
} }
} }
message RequestPluginPermissionPayload {
repeated plugin_permission.PermissionType permissions = 1;
}
message SubscribePayload { message SubscribePayload {
event.EventNameList subscriptions = 1; event.EventNameList subscriptions = 1;
} }

View file

@ -3,14 +3,15 @@ pub use super::generated_api::api::{
plugin_command::{ plugin_command::{
plugin_command::Payload, CommandName, ExecCmdPayload, IdAndNewName, MovePayload, plugin_command::Payload, CommandName, ExecCmdPayload, IdAndNewName, MovePayload,
OpenCommandPanePayload, OpenFilePayload, PaneIdAndShouldFloat, OpenCommandPanePayload, OpenFilePayload, PaneIdAndShouldFloat,
PluginCommand as ProtobufPluginCommand, PluginMessagePayload, ResizePayload, PluginCommand as ProtobufPluginCommand, PluginMessagePayload,
SetTimeoutPayload, SubscribePayload, SwitchTabToPayload, SwitchToModePayload, RequestPluginPermissionPayload, ResizePayload, SetTimeoutPayload, SubscribePayload,
UnsubscribePayload, SwitchTabToPayload, SwitchToModePayload, UnsubscribePayload,
}, },
plugin_permission::PermissionType as ProtobufPermissionType,
resize::ResizeAction as ProtobufResizeAction, resize::ResizeAction as ProtobufResizeAction,
}; };
use crate::data::PluginCommand; use crate::data::{PermissionType, PluginCommand};
use std::convert::TryFrom; use std::convert::TryFrom;
@ -486,6 +487,19 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
}, },
_ => Err("Mismatched payload for ReportCrash"), _ => Err("Mismatched payload for ReportCrash"),
}, },
Some(CommandName::RequestPluginPermissions) => match protobuf_plugin_command.payload {
Some(Payload::RequestPluginPermissionPayload(payload)) => {
Ok(PluginCommand::RequestPluginPermissions(
payload
.permissions
.iter()
.filter_map(|p| ProtobufPermissionType::from_i32(*p))
.filter_map(|p| PermissionType::try_from(p).ok())
.collect(),
))
},
_ => Err("Mismatched payload for RequestPluginPermission"),
},
None => Err("Unrecognized plugin command"), None => Err("Unrecognized plugin command"),
} }
} }
@ -820,6 +834,18 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
name: CommandName::ReportCrash as i32, name: CommandName::ReportCrash as i32,
payload: Some(Payload::ReportCrashPayload(payload)), payload: Some(Payload::ReportCrashPayload(payload)),
}), }),
PluginCommand::RequestPluginPermissions(permissions) => Ok(ProtobufPluginCommand {
name: CommandName::RequestPluginPermissions as i32,
payload: Some(Payload::RequestPluginPermissionPayload(
RequestPluginPermissionPayload {
permissions: permissions
.iter()
.filter_map(|p| ProtobufPermissionType::try_from(*p).ok())
.map(|p| p as i32)
.collect(),
},
)),
}),
} }
} }
} }

View file

@ -0,0 +1,12 @@
syntax = "proto3";
package api.plugin_permission;
enum PermissionType {
ReadApplicationState = 0;
ChangeApplicationState = 1;
OpenFiles = 2;
RunCommands = 3;
OpenTerminalsOrPlugins = 4;
WriteToStdin = 5;
}

View file

@ -0,0 +1,44 @@
pub use super::generated_api::api::plugin_permission::PermissionType as ProtobufPermissionType;
use crate::data::PermissionType;
use std::convert::TryFrom;
impl TryFrom<ProtobufPermissionType> for PermissionType {
type Error = &'static str;
fn try_from(protobuf_permission: ProtobufPermissionType) -> Result<Self, &'static str> {
match protobuf_permission {
ProtobufPermissionType::ReadApplicationState => {
Ok(PermissionType::ReadApplicationState)
},
ProtobufPermissionType::ChangeApplicationState => {
Ok(PermissionType::ChangeApplicationState)
},
ProtobufPermissionType::OpenFiles => Ok(PermissionType::OpenFiles),
ProtobufPermissionType::RunCommands => Ok(PermissionType::RunCommands),
ProtobufPermissionType::OpenTerminalsOrPlugins => {
Ok(PermissionType::OpenTerminalsOrPlugins)
},
ProtobufPermissionType::WriteToStdin => Ok(PermissionType::WriteToStdin),
}
}
}
impl TryFrom<PermissionType> for ProtobufPermissionType {
type Error = &'static str;
fn try_from(permission: PermissionType) -> Result<Self, &'static str> {
match permission {
PermissionType::ReadApplicationState => {
Ok(ProtobufPermissionType::ReadApplicationState)
},
PermissionType::ChangeApplicationState => {
Ok(ProtobufPermissionType::ChangeApplicationState)
},
PermissionType::OpenFiles => Ok(ProtobufPermissionType::OpenFiles),
PermissionType::RunCommands => Ok(ProtobufPermissionType::RunCommands),
PermissionType::OpenTerminalsOrPlugins => {
Ok(ProtobufPermissionType::OpenTerminalsOrPlugins)
},
PermissionType::WriteToStdin => Ok(ProtobufPermissionType::WriteToStdin),
}
}
}