1544 lines · 57.1 KB · Rust
RawDownload
//! A module, responsible for managing the trust logic in Zed.//!//! It deals with multiple hosts, distinguished by [`RemoteHostLocation`].//! Each [`crate::Project`] and `HeadlessProject` should call [`init_global`], if wants to establish the trust mechanism.//! This will set up a [`gpui::Global`] with [`TrustedWorktrees`] entity that will persist, restore and allow querying for worktree trust.//! It's also possible to subscribe on [`TrustedWorktreesEvent`] events of this entity to track trust changes dynamically.//!//! The implementation can synchronize trust information with the remote hosts: currently, WSL and SSH.//! Docker and Collab remotes do not employ trust mechanism, as manage that themselves.//!//! Unless `trust_all_worktrees` auto trust is enabled, does not trust anything that was not persisted before.//! When dealing with "restricted" and other related concepts in the API, it means all explicitly restricted, after any of the [`TrustedWorktreesStore::can_trust`] and [`TrustedWorktreesStore::can_trust_global`] calls.//!//!//!//!//! Path rust hierarchy.//!//! Zed has multiple layers of trust, based on the requests and [`PathTrust`] enum variants.//! From the least to the most trusted level://!//! * "single file worktree"//!//! After opening an empty Zed it's possible to open just a file, same as after opening a directory in Zed it's possible to open a file outside of this directory.//! Usual scenario for both cases is opening Zed's settings.json file via `zed: open settings file` command: that starts a language server for a new file open, which originates from a newly created, single file worktree.//!//! Spawning a language server is potentially dangerous, and Zed needs to restrict that by default.//! Each single file worktree requires a separate trust permission, unless a more global level is trusted.//!//! * "directory worktree"//!//! If a directory is open in Zed, it's a full worktree which may spawn multiple language servers associated with it.//! Each such worktree requires a separate trust permission, so each separate directory worktree has to be trusted separately, unless a more global level is trusted.//!//! When a directory worktree is trusted and language servers are allowed to be downloaded and started, hence, "single file worktree" level of trust also.//!//! * "path override"//!//! To ease trusting multiple directory worktrees at once, it's possible to trust a parent directory of a certain directory worktree opened in Zed.//! Trusting a directory means trusting all its subdirectories as well, including all current and potential directory worktrees. use client::ProjectId;use collections::{HashMap, HashSet};use gpui::{    App, AppContext as _, Context, Entity, EventEmitter, Global, SharedString, Task, WeakEntity,};use remote::RemoteConnectionOptions;use rpc::{AnyProtoClient, proto};use settings::{Settings as _, WorktreeId};use std::{    path::{Path, PathBuf},    sync::Arc,};use util::debug_panic; use crate::{project_settings::ProjectSettings, worktree_store::WorktreeStore}; pub fn init(    db_trusted_paths: DbTrustedPaths,    downstream_client: Option<(AnyProtoClient, ProjectId)>,    upstream_client: Option<(AnyProtoClient, ProjectId)>,    cx: &mut App,) {    if TrustedWorktrees::try_get_global(cx).is_none() {        let trusted_worktrees = cx.new(|_| {            TrustedWorktreesStore::new(db_trusted_paths, downstream_client, upstream_client)        });        cx.set_global(TrustedWorktrees(trusted_worktrees))    }} /// An initialization call to set up trust global for a particular project (remote or local).pub fn track_worktree_trust(    worktree_store: Entity<WorktreeStore>,    remote_host: Option<RemoteHostLocation>,    downstream_client: Option<(AnyProtoClient, ProjectId)>,    upstream_client: Option<(AnyProtoClient, ProjectId)>,    cx: &mut App,) {    match TrustedWorktrees::try_get_global(cx) {        Some(trusted_worktrees) => {            trusted_worktrees.update(cx, |trusted_worktrees, cx| {                if let Some(downstream_client) = downstream_client {                    trusted_worktrees.downstream_clients.push(downstream_client);                }                if let Some(upstream_client) = upstream_client.clone() {                    trusted_worktrees.upstream_clients.push(upstream_client);                }                trusted_worktrees.add_worktree_store(worktree_store, remote_host, cx);                 if let Some((upstream_client, upstream_project_id)) = upstream_client {                    let trusted_paths = trusted_worktrees                        .trusted_paths                        .iter()                        .flat_map(|(_, paths)| {                            paths.iter().map(|trusted_path| trusted_path.to_proto())                        })                        .collect::<Vec<_>>();                    if !trusted_paths.is_empty() {                        upstream_client                            .send(proto::TrustWorktrees {                                project_id: upstream_project_id.0,                                trusted_paths,                            })                            .ok();                    }                }            });        }        None => log::debug!("No TrustedWorktrees initialized, not tracking worktree trust"),    }} /// A collection of worktree trust metadata, can be accessed globally (if initialized) and subscribed to.pub struct TrustedWorktrees(Entity<TrustedWorktreesStore>); impl Global for TrustedWorktrees {} impl TrustedWorktrees {    pub fn try_get_global(cx: &App) -> Option<Entity<TrustedWorktreesStore>> {        cx.try_global::<Self>().map(|this| this.0.clone())    }} /// A collection of worktrees that are considered trusted and not trusted./// This can be used when checking for this criteria before enabling certain features.////// Emits an event each time the worktree was checked and found not trusted,/// or a certain worktree had been trusted.#[derive(Debug)]pub struct TrustedWorktreesStore {    downstream_clients: Vec<(AnyProtoClient, ProjectId)>,    upstream_clients: Vec<(AnyProtoClient, ProjectId)>,    worktree_stores: HashMap<WeakEntity<WorktreeStore>, Option<RemoteHostLocation>>,    db_trusted_paths: DbTrustedPaths,    trusted_paths: TrustedPaths,    restricted: HashMap<WeakEntity<WorktreeStore>, HashSet<WorktreeId>>,    worktree_trust_serialization: Task<()>,} /// An identifier of a host to split the trust questions by./// Each trusted data change and event is done for a particular host./// A host may contain more than one worktree or even project open concurrently.#[derive(Debug, PartialEq, Eq, Clone, Hash)]pub struct RemoteHostLocation {    pub user_name: Option<SharedString>,    pub host_identifier: SharedString,} impl From<RemoteConnectionOptions> for RemoteHostLocation {    fn from(options: RemoteConnectionOptions) -> Self {        let (user_name, host_name) = match options {            RemoteConnectionOptions::Ssh(ssh) => (                ssh.username.map(SharedString::new),                SharedString::new(ssh.host.to_string()),            ),            RemoteConnectionOptions::Wsl(wsl) => (                wsl.user.map(SharedString::new),                SharedString::new(wsl.distro_name),            ),            RemoteConnectionOptions::Docker(docker_connection_options) => (                Some(SharedString::new(docker_connection_options.name)),                SharedString::new(docker_connection_options.container_id),            ),        };        Self {            user_name,            host_identifier: host_name,        }    }} /// A unit of trust consideration inside a particular host:/// either a familiar worktree, or a path that may influence other worktrees' trust./// See module-level documentation on the trust model.#[derive(Debug, PartialEq, Eq, Clone, Hash)]pub enum PathTrust {    /// A worktree that is familiar to this workspace.    /// Either a single file or a directory worktree.    Worktree(WorktreeId),    /// A path that may be another worktree yet not loaded into any workspace (hence, without any `WorktreeId`),    /// or a parent path coming out of the security modal.    AbsPath(PathBuf),} impl PathTrust {    fn to_proto(&self) -> proto::PathTrust {        match self {            Self::Worktree(worktree_id) => proto::PathTrust {                content: Some(proto::path_trust::Content::WorktreeId(                    worktree_id.to_proto(),                )),            },            Self::AbsPath(path_buf) => proto::PathTrust {                content: Some(proto::path_trust::Content::AbsPath(                    path_buf.to_string_lossy().to_string(),                )),            },        }    }     pub fn from_proto(proto: proto::PathTrust) -> Option<Self> {        Some(match proto.content? {            proto::path_trust::Content::WorktreeId(id) => {                Self::Worktree(WorktreeId::from_proto(id))            }            proto::path_trust::Content::AbsPath(path) => Self::AbsPath(PathBuf::from(path)),        })    }} /// A change of trust on a certain host.#[derive(Debug)]pub enum TrustedWorktreesEvent {    Trusted(WeakEntity<WorktreeStore>, HashSet<PathTrust>),    Restricted(WeakEntity<WorktreeStore>, HashSet<PathTrust>),} impl EventEmitter<TrustedWorktreesEvent> for TrustedWorktreesStore {} type TrustedPaths = HashMap<WeakEntity<WorktreeStore>, HashSet<PathTrust>>;pub type DbTrustedPaths = HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>; impl TrustedWorktreesStore {    fn new(        db_trusted_paths: DbTrustedPaths,        downstream_client: Option<(AnyProtoClient, ProjectId)>,        upstream_client: Option<(AnyProtoClient, ProjectId)>,    ) -> Self {        if let Some((upstream_client, upstream_project_id)) = &upstream_client {            let trusted_paths = db_trusted_paths                .iter()                .flat_map(|(_, paths)| {                    paths                        .iter()                        .cloned()                        .map(PathTrust::AbsPath)                        .map(|trusted_path| trusted_path.to_proto())                })                .collect::<Vec<_>>();            if !trusted_paths.is_empty() {                upstream_client                    .send(proto::TrustWorktrees {                        project_id: upstream_project_id.0,                        trusted_paths,                    })                    .ok();            }        }         Self {            db_trusted_paths,            downstream_clients: downstream_client.into_iter().collect(),            upstream_clients: upstream_client.into_iter().collect(),            trusted_paths: HashMap::default(),            worktree_stores: HashMap::default(),            restricted: HashMap::default(),            worktree_trust_serialization: Task::ready(()),        }    }     /// Whether a particular worktree store has associated worktrees that are restricted, or an associated host is restricted.    pub fn has_restricted_worktrees(        &self,        worktree_store: &Entity<WorktreeStore>,        cx: &App,    ) -> bool {        self.restricted            .get(&worktree_store.downgrade())            .is_some_and(|restricted_worktrees| {                restricted_worktrees.iter().any(|restricted_worktree| {                    worktree_store                        .read(cx)                        .worktree_for_id(*restricted_worktree, cx)                        .is_some()                })            })    }     /// Adds certain entities on this host to the trusted list.    /// This will emit [`TrustedWorktreesEvent::Trusted`] event for all passed entries    /// and the ones that got auto trusted based on trust hierarchy (see module-level docs).    pub fn trust(        &mut self,        worktree_store: &Entity<WorktreeStore>,        mut trusted_paths: HashSet<PathTrust>,        cx: &mut Context<Self>,    ) {        let weak_worktree_store = worktree_store.downgrade();        let mut new_trusted_single_file_worktrees = HashSet::default();        let mut new_trusted_other_worktrees = HashSet::default();        let mut new_trusted_abs_paths = HashSet::default();        for trusted_path in trusted_paths.iter().chain(            self.trusted_paths                .remove(&weak_worktree_store)                .iter()                .flat_map(|current_trusted| current_trusted.iter()),        ) {            match trusted_path {                PathTrust::Worktree(worktree_id) => {                    if let Some(restricted_worktrees) =                        self.restricted.get_mut(&weak_worktree_store)                    {                        restricted_worktrees.remove(worktree_id);                    };                     if let Some(worktree) =                        worktree_store.read(cx).worktree_for_id(*worktree_id, cx)                    {                        if worktree.read(cx).is_single_file() {                            new_trusted_single_file_worktrees.insert(*worktree_id);                        } else {                            new_trusted_other_worktrees                                .insert((worktree.read(cx).abs_path(), *worktree_id));                        }                    }                }                PathTrust::AbsPath(abs_path) => {                    debug_assert!(                        abs_path.is_absolute(),                        "Cannot trust non-absolute path {abs_path:?}"                    );                    if let Some((worktree_id, is_file)) =                        find_worktree_in_store(worktree_store.read(cx), abs_path, cx)                    {                        if is_file {                            new_trusted_single_file_worktrees.insert(worktree_id);                        } else {                            new_trusted_other_worktrees                                .insert((Arc::from(abs_path.as_path()), worktree_id));                        }                    }                    new_trusted_abs_paths.insert(abs_path.clone());                }            }        }         new_trusted_other_worktrees.retain(|(worktree_abs_path, _)| {            new_trusted_abs_paths                .iter()                .all(|new_trusted_path| !worktree_abs_path.starts_with(new_trusted_path))        });        if !new_trusted_other_worktrees.is_empty() {            new_trusted_single_file_worktrees.clear();        }         if let Some(restricted_worktrees) = self.restricted.remove(&weak_worktree_store) {            let new_restricted_worktrees = restricted_worktrees                .into_iter()                .filter(|restricted_worktree| {                    let Some(worktree) = worktree_store                        .read(cx)                        .worktree_for_id(*restricted_worktree, cx)                    else {                        return false;                    };                    let is_file = worktree.read(cx).is_single_file();                     // When trusting an abs path on the host, we transitively trust all single file worktrees on this host too.                    if is_file && !new_trusted_abs_paths.is_empty() {                        trusted_paths.insert(PathTrust::Worktree(*restricted_worktree));                        return false;                    }                     let restricted_worktree_path = worktree.read(cx).abs_path();                    let retain = (!is_file || new_trusted_other_worktrees.is_empty())                        && new_trusted_abs_paths.iter().all(|new_trusted_path| {                            !restricted_worktree_path.starts_with(new_trusted_path)                        });                    if !retain {                        trusted_paths.insert(PathTrust::Worktree(*restricted_worktree));                    }                    retain                })                .collect();            self.restricted                .insert(weak_worktree_store.clone(), new_restricted_worktrees);        }         {            let trusted_paths = self                .trusted_paths                .entry(weak_worktree_store.clone())                .or_default();            trusted_paths.extend(new_trusted_abs_paths.into_iter().map(PathTrust::AbsPath));            trusted_paths.extend(                new_trusted_other_worktrees                    .into_iter()                    .map(|(_, worktree_id)| PathTrust::Worktree(worktree_id)),            );            trusted_paths.extend(                new_trusted_single_file_worktrees                    .into_iter()                    .map(PathTrust::Worktree),            );        }         cx.emit(TrustedWorktreesEvent::Trusted(            weak_worktree_store,            trusted_paths.clone(),        ));         for (upstream_client, upstream_project_id) in &self.upstream_clients {            let trusted_paths = trusted_paths                .iter()                .map(|trusted_path| trusted_path.to_proto())                .collect::<Vec<_>>();            if !trusted_paths.is_empty() {                upstream_client                    .send(proto::TrustWorktrees {                        project_id: upstream_project_id.0,                        trusted_paths,                    })                    .ok();            }        }    }     /// Restricts certain entities on this host.    /// This will emit [`TrustedWorktreesEvent::Restricted`] event for all passed entries.    pub fn restrict(        &mut self,        worktree_store: WeakEntity<WorktreeStore>,        restricted_paths: HashSet<PathTrust>,        cx: &mut Context<Self>,    ) {        let mut restricted = HashSet::default();        for restricted_path in restricted_paths {            match restricted_path {                PathTrust::Worktree(worktree_id) => {                    self.restricted                        .entry(worktree_store.clone())                        .or_default()                        .insert(worktree_id);                    restricted.insert(PathTrust::Worktree(worktree_id));                }                PathTrust::AbsPath(..) => debug_panic!("Unexpected: cannot restrict an abs path"),            }        }         cx.emit(TrustedWorktreesEvent::Restricted(            worktree_store,            restricted,        ));    }     /// Erases all trust information.    /// Requires Zed's restart to take proper effect.    pub fn clear_trusted_paths(&mut self) {        self.trusted_paths.clear();        self.db_trusted_paths.clear();    }     /// Checks whether a certain worktree is trusted (or on a larger trust level).    /// If not, emits [`TrustedWorktreesEvent::Restricted`] event if for the first time and not trusted, or no corresponding worktree store was found.    ///    /// No events or data adjustment happens when `trust_all_worktrees` auto trust is enabled.    pub fn can_trust(        &mut self,        worktree_store: &Entity<WorktreeStore>,        worktree_id: WorktreeId,        cx: &mut Context<Self>,    ) -> bool {        if ProjectSettings::get_global(cx).session.trust_all_worktrees {            return true;        }         let weak_worktree_store = worktree_store.downgrade();        let Some(worktree) = worktree_store.read(cx).worktree_for_id(worktree_id, cx) else {            return false;        };        let worktree_path = worktree.read(cx).abs_path();        let is_file = worktree.read(cx).is_single_file();        if self            .restricted            .get(&weak_worktree_store)            .is_some_and(|restricted_worktrees| restricted_worktrees.contains(&worktree_id))        {            return false;        }         if self            .trusted_paths            .get(&weak_worktree_store)            .is_some_and(|trusted_paths| trusted_paths.contains(&PathTrust::Worktree(worktree_id)))        {            return true;        }         // See module documentation for details on trust level.        if is_file && self.trusted_paths.contains_key(&weak_worktree_store) {            return true;        }         let parent_path_trusted =            self.trusted_paths                .get(&weak_worktree_store)                .is_some_and(|trusted_paths| {                    trusted_paths.iter().any(|trusted_path| {                        let PathTrust::AbsPath(trusted_path) = trusted_path else {

Lines 1–500 of 1544.