disallow opening private files (#7165)
- Disallow sharing gitignored files through collab - Show errors when failing to open files - Show a warning to followers when view is unshared /cc @mikaylamaki, let's update this to use your `private_files` config before merge. Release Notes: - Added the ability to prevent sharing private files over collab. --------- Co-authored-by: Piotr <piotr@zed.dev> Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
parent
c983c9b6df
commit
dcca48482b
9 changed files with 101 additions and 19 deletions
|
@ -28,6 +28,9 @@ async fn main() -> Result<()> {
|
||||||
Some("version") => {
|
Some("version") => {
|
||||||
println!("collab v{VERSION}");
|
println!("collab v{VERSION}");
|
||||||
}
|
}
|
||||||
|
Some("migrate") => {
|
||||||
|
run_migrations().await?;
|
||||||
|
}
|
||||||
Some("serve") => {
|
Some("serve") => {
|
||||||
let config = envy::from_env::<Config>().expect("error loading config");
|
let config = envy::from_env::<Config>().expect("error loading config");
|
||||||
init_tracing(&config);
|
init_tracing(&config);
|
||||||
|
|
|
@ -185,6 +185,14 @@ impl FollowableItem for Editor {
|
||||||
|
|
||||||
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
|
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
|
||||||
let buffer = self.buffer.read(cx);
|
let buffer = self.buffer.read(cx);
|
||||||
|
if buffer
|
||||||
|
.as_singleton()
|
||||||
|
.and_then(|buffer| buffer.read(cx).file())
|
||||||
|
.map_or(false, |file| file.is_private())
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let scroll_anchor = self.scroll_manager.anchor();
|
let scroll_anchor = self.scroll_manager.anchor();
|
||||||
let excerpts = buffer
|
let excerpts = buffer
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
|
|
@ -384,7 +384,7 @@ pub trait File: Send + Sync {
|
||||||
/// Converts this file into a protobuf message.
|
/// Converts this file into a protobuf message.
|
||||||
fn to_proto(&self) -> rpc::proto::File;
|
fn to_proto(&self) -> rpc::proto::File;
|
||||||
|
|
||||||
/// Return whether Zed considers this to be a dotenv file.
|
/// Return whether Zed considers this to be a private file.
|
||||||
fn is_private(&self) -> bool;
|
fn is_private(&self) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -406,6 +406,11 @@ pub trait LocalFile: File {
|
||||||
mtime: SystemTime,
|
mtime: SystemTime,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Returns true if the file should not be shared with collaborators.
|
||||||
|
fn is_private(&self, _: &AppContext) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The auto-indent behavior associated with an editing operation.
|
/// The auto-indent behavior associated with an editing operation.
|
||||||
|
|
|
@ -56,6 +56,7 @@ use postage::watch;
|
||||||
use prettier_support::{DefaultPrettier, PrettierInstance};
|
use prettier_support::{DefaultPrettier, PrettierInstance};
|
||||||
use project_settings::{LspSettings, ProjectSettings};
|
use project_settings::{LspSettings, ProjectSettings};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
|
use rpc::{ErrorCode, ErrorExt};
|
||||||
use search::SearchQuery;
|
use search::SearchQuery;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
|
@ -1760,7 +1761,7 @@ impl Project {
|
||||||
cx.background_executor().spawn(async move {
|
cx.background_executor().spawn(async move {
|
||||||
wait_for_loading_buffer(loading_watch)
|
wait_for_loading_buffer(loading_watch)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| anyhow!("{project_path:?} opening failure: {error:#}"))
|
.map_err(|e| e.cloned())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8011,11 +8012,20 @@ impl Project {
|
||||||
.update(&mut cx, |this, cx| this.open_buffer_for_symbol(&symbol, cx))?
|
.update(&mut cx, |this, cx| this.open_buffer_for_symbol(&symbol, cx))?
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(proto::OpenBufferForSymbolResponse {
|
this.update(&mut cx, |this, cx| {
|
||||||
buffer_id: this.update(&mut cx, |this, cx| {
|
let is_private = buffer
|
||||||
this.create_buffer_for_peer(&buffer, peer_id, cx).into()
|
.read(cx)
|
||||||
})?,
|
.file()
|
||||||
})
|
.map(|f| f.is_private())
|
||||||
|
.unwrap_or_default();
|
||||||
|
if is_private {
|
||||||
|
Err(anyhow!(ErrorCode::UnsharedItem))
|
||||||
|
} else {
|
||||||
|
Ok(proto::OpenBufferForSymbolResponse {
|
||||||
|
buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx).into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})?
|
||||||
}
|
}
|
||||||
|
|
||||||
fn symbol_signature(&self, project_path: &ProjectPath) -> [u8; 32] {
|
fn symbol_signature(&self, project_path: &ProjectPath) -> [u8; 32] {
|
||||||
|
@ -8037,11 +8047,7 @@ impl Project {
|
||||||
let buffer = this
|
let buffer = this
|
||||||
.update(&mut cx, |this, cx| this.open_buffer_by_id(buffer_id, cx))?
|
.update(&mut cx, |this, cx| this.open_buffer_by_id(buffer_id, cx))?
|
||||||
.await?;
|
.await?;
|
||||||
this.update(&mut cx, |this, cx| {
|
Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx)
|
||||||
Ok(proto::OpenBufferResponse {
|
|
||||||
buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx).into(),
|
|
||||||
})
|
|
||||||
})?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_open_buffer_by_path(
|
async fn handle_open_buffer_by_path(
|
||||||
|
@ -8063,10 +8069,28 @@ impl Project {
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let buffer = open_buffer.await?;
|
let buffer = open_buffer.await?;
|
||||||
this.update(&mut cx, |this, cx| {
|
Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx)
|
||||||
Ok(proto::OpenBufferResponse {
|
}
|
||||||
buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx).into(),
|
|
||||||
})
|
fn respond_to_open_buffer_request(
|
||||||
|
this: Model<Self>,
|
||||||
|
buffer: Model<Buffer>,
|
||||||
|
peer_id: proto::PeerId,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Result<proto::OpenBufferResponse> {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
let is_private = buffer
|
||||||
|
.read(cx)
|
||||||
|
.file()
|
||||||
|
.map(|f| f.is_private())
|
||||||
|
.unwrap_or_default();
|
||||||
|
if is_private {
|
||||||
|
Err(anyhow!(ErrorCode::UnsharedItem))
|
||||||
|
} else {
|
||||||
|
Ok(proto::OpenBufferResponse {
|
||||||
|
buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx).into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
})?
|
})?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ theme = { path = "../theme" }
|
||||||
ui = { path = "../ui" }
|
ui = { path = "../ui" }
|
||||||
unicase = "2.6"
|
unicase = "2.6"
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
|
client = { path = "../client" }
|
||||||
workspace = { path = "../workspace", package = "workspace" }
|
workspace = { path = "../workspace", package = "workspace" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
pub mod file_associations;
|
pub mod file_associations;
|
||||||
mod project_panel_settings;
|
mod project_panel_settings;
|
||||||
|
use client::{ErrorCode, ErrorExt};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
|
@ -35,6 +36,7 @@ use unicase::UniCase;
|
||||||
use util::{maybe, ResultExt, TryFutureExt};
|
use util::{maybe, ResultExt, TryFutureExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
|
notifications::DetachAndPromptErr,
|
||||||
Workspace,
|
Workspace,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -259,6 +261,7 @@ impl ProjectPanel {
|
||||||
} => {
|
} => {
|
||||||
if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
|
if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
|
||||||
if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
|
if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
|
||||||
|
let file_path = entry.path.clone();
|
||||||
workspace
|
workspace
|
||||||
.open_path(
|
.open_path(
|
||||||
ProjectPath {
|
ProjectPath {
|
||||||
|
@ -269,7 +272,15 @@ impl ProjectPanel {
|
||||||
focus_opened_item,
|
focus_opened_item,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.detach_and_log_err(cx);
|
.detach_and_prompt_err("Failed to open file", cx, move |e, _| {
|
||||||
|
match e.error_code() {
|
||||||
|
ErrorCode::UnsharedItem => Some(format!(
|
||||||
|
"{} is not shared by the host. This could be because it has been marked as `private`",
|
||||||
|
file_path.display()
|
||||||
|
)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
});
|
||||||
if !focus_opened_item {
|
if !focus_opened_item {
|
||||||
if let Some(project_panel) = project_panel.upgrade() {
|
if let Some(project_panel) = project_panel.upgrade() {
|
||||||
let focus_handle = project_panel.read(cx).focus_handle.clone();
|
let focus_handle = project_panel.read(cx).focus_handle.clone();
|
||||||
|
|
|
@ -216,6 +216,7 @@ enum ErrorCode {
|
||||||
BadPublicNesting = 9;
|
BadPublicNesting = 9;
|
||||||
CircularNesting = 10;
|
CircularNesting = 10;
|
||||||
WrongMoveTarget = 11;
|
WrongMoveTarget = 11;
|
||||||
|
UnsharedItem = 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Test {
|
message Test {
|
||||||
|
|
|
@ -80,6 +80,8 @@ pub trait ErrorExt {
|
||||||
fn error_tag(&self, k: &str) -> Option<&str>;
|
fn error_tag(&self, k: &str) -> Option<&str>;
|
||||||
/// to_proto() converts the error into a proto::Error
|
/// to_proto() converts the error into a proto::Error
|
||||||
fn to_proto(&self) -> proto::Error;
|
fn to_proto(&self) -> proto::Error;
|
||||||
|
///
|
||||||
|
fn cloned(&self) -> anyhow::Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ErrorExt for anyhow::Error {
|
impl ErrorExt for anyhow::Error {
|
||||||
|
@ -106,6 +108,14 @@ impl ErrorExt for anyhow::Error {
|
||||||
ErrorCode::Internal.message(format!("{}", self)).to_proto()
|
ErrorCode::Internal.message(format!("{}", self)).to_proto()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cloned(&self) -> anyhow::Error {
|
||||||
|
if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
|
||||||
|
rpc_error.cloned()
|
||||||
|
} else {
|
||||||
|
anyhow::anyhow!("{}", self)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<proto::ErrorCode> for anyhow::Error {
|
impl From<proto::ErrorCode> for anyhow::Error {
|
||||||
|
@ -189,6 +199,10 @@ impl ErrorExt for RpcError {
|
||||||
tags: self.tags.clone(),
|
tags: self.tags.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cloned(&self) -> anyhow::Error {
|
||||||
|
self.clone().into()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for RpcError {
|
impl std::error::Error for RpcError {
|
||||||
|
|
|
@ -176,11 +176,19 @@ impl Member {
|
||||||
return div().into_any();
|
return div().into_any();
|
||||||
}
|
}
|
||||||
|
|
||||||
let leader = follower_states.get(pane).and_then(|state| {
|
let follower_state = follower_states.get(pane);
|
||||||
|
|
||||||
|
let leader = follower_state.and_then(|state| {
|
||||||
let room = active_call?.read(cx).room()?.read(cx);
|
let room = active_call?.read(cx).room()?.read(cx);
|
||||||
room.remote_participant_for_peer_id(state.leader_id)
|
room.remote_participant_for_peer_id(state.leader_id)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let is_in_unshared_view = follower_state.map_or(false, |state| {
|
||||||
|
state.active_view_id.is_some_and(|view_id| {
|
||||||
|
!state.items_by_leader_view_id.contains_key(&view_id)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
let mut leader_border = None;
|
let mut leader_border = None;
|
||||||
let mut leader_status_box = None;
|
let mut leader_status_box = None;
|
||||||
let mut leader_join_data = None;
|
let mut leader_join_data = None;
|
||||||
|
@ -198,7 +206,14 @@ impl Member {
|
||||||
project_id: leader_project_id,
|
project_id: leader_project_id,
|
||||||
} => {
|
} => {
|
||||||
if Some(leader_project_id) == project.read(cx).remote_id() {
|
if Some(leader_project_id) == project.read(cx).remote_id() {
|
||||||
None
|
if is_in_unshared_view {
|
||||||
|
Some(Label::new(format!(
|
||||||
|
"{} is in an unshared pane",
|
||||||
|
leader.user.github_login
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
leader_join_data = Some((leader_project_id, leader.user.id));
|
leader_join_data = Some((leader_project_id, leader.user.id));
|
||||||
Some(Label::new(format!(
|
Some(Label::new(format!(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue