Small worktree trust fixes (#45500) (cherry-pick to preview) (#45501)

zed-zippy[bot] and Kirill Bulatov created

Cherry-pick of #45500 to preview

----
* Abs path trust should transitively trust all single file worktrees on
the same host
* Init worktree trust on the client side even when devcontainers are
run: remote host unconditionally checks trust, hence the client has to
keep track of it and respond with approves/declines.
Do trust all devcontainers' remote worktrees, as containers are isolated
and "safe".

Release Notes:

- N/A

Co-authored-by: Kirill Bulatov <kirill@zed.dev>

Change summary

crates/project/src/project.rs           | 36 ++++++++---
crates/project/src/trusted_worktrees.rs | 86 +++++++++++++++++++++++++++
2 files changed, 112 insertions(+), 10 deletions(-)

Detailed changes

crates/project/src/project.rs 🔗

@@ -1291,17 +1291,33 @@ impl Project {
             cx.subscribe(&worktree_store, Self::on_worktree_store_event)
                 .detach();
             if init_worktree_trust {
-                match &connection_options {
-                    RemoteConnectionOptions::Wsl(..) | RemoteConnectionOptions::Ssh(..) => {
-                        trusted_worktrees::track_worktree_trust(
-                            worktree_store.clone(),
-                            Some(RemoteHostLocation::from(connection_options)),
-                            None,
-                            Some((remote_proto.clone(), REMOTE_SERVER_PROJECT_ID)),
-                            cx,
-                        );
+                let trust_remote_project = match &connection_options {
+                    RemoteConnectionOptions::Ssh(..) | RemoteConnectionOptions::Wsl(..) => false,
+                    RemoteConnectionOptions::Docker(..) => true,
+                };
+                let remote_host = RemoteHostLocation::from(connection_options);
+                trusted_worktrees::track_worktree_trust(
+                    worktree_store.clone(),
+                    Some(remote_host.clone()),
+                    None,
+                    Some((remote_proto.clone(), REMOTE_SERVER_PROJECT_ID)),
+                    cx,
+                );
+                if trust_remote_project {
+                    if let Some(trusted_worktres) = TrustedWorktrees::try_get_global(cx) {
+                        trusted_worktres.update(cx, |trusted_worktres, cx| {
+                            trusted_worktres.trust(
+                                worktree_store
+                                    .read(cx)
+                                    .worktrees()
+                                    .map(|worktree| worktree.read(cx).id())
+                                    .map(PathTrust::Worktree)
+                                    .collect(),
+                                Some(remote_host),
+                                cx,
+                            );
+                        })
                     }
-                    RemoteConnectionOptions::Docker(..) => {}
                 }
             }
 

crates/project/src/trusted_worktrees.rs 🔗

@@ -337,6 +337,13 @@ impl TrustedWorktreesStore {
                 if restricted_host != remote_host {
                     return true;
                 }
+
+                // 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 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)
@@ -1045,6 +1052,13 @@ mod tests {
             "single-file worktree should be restricted initially"
         );
 
+        let can_trust_directory =
+            trusted_worktrees.update(cx, |store, cx| store.can_trust(dir_worktree_id, cx));
+        assert!(
+            !can_trust_directory,
+            "directory worktree should be restricted initially"
+        );
+
         trusted_worktrees.update(cx, |store, cx| {
             store.trust(
                 HashSet::from_iter([PathTrust::Worktree(dir_worktree_id)]),
@@ -1064,6 +1078,78 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_parent_path_trust_enables_single_file(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            path!("/"),
+            json!({
+                "project": { "main.rs": "fn main() {}" },
+                "standalone.rs": "fn standalone() {}"
+            }),
+        )
+        .await;
+
+        let project = Project::test(
+            fs,
+            [path!("/project").as_ref(), path!("/standalone.rs").as_ref()],
+            cx,
+        )
+        .await;
+        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
+        let (dir_worktree_id, file_worktree_id) = worktree_store.read_with(cx, |store, cx| {
+            let worktrees: Vec<_> = store.worktrees().collect();
+            assert_eq!(worktrees.len(), 2);
+            let (dir_worktree, file_worktree) = if worktrees[0].read(cx).is_single_file() {
+                (&worktrees[1], &worktrees[0])
+            } else {
+                (&worktrees[0], &worktrees[1])
+            };
+            assert!(!dir_worktree.read(cx).is_single_file());
+            assert!(file_worktree.read(cx).is_single_file());
+            (dir_worktree.read(cx).id(), file_worktree.read(cx).id())
+        });
+
+        let trusted_worktrees = init_trust_global(worktree_store, cx);
+
+        let can_trust_file =
+            trusted_worktrees.update(cx, |store, cx| store.can_trust(file_worktree_id, cx));
+        assert!(
+            !can_trust_file,
+            "single-file worktree should be restricted initially"
+        );
+
+        let can_trust_directory =
+            trusted_worktrees.update(cx, |store, cx| store.can_trust(dir_worktree_id, cx));
+        assert!(
+            !can_trust_directory,
+            "directory worktree should be restricted initially"
+        );
+
+        trusted_worktrees.update(cx, |store, cx| {
+            store.trust(
+                HashSet::from_iter([PathTrust::AbsPath(PathBuf::from(path!("/project")))]),
+                None,
+                cx,
+            );
+        });
+
+        let can_trust_dir =
+            trusted_worktrees.update(cx, |store, cx| store.can_trust(dir_worktree_id, cx));
+        let can_trust_file_after =
+            trusted_worktrees.update(cx, |store, cx| store.can_trust(file_worktree_id, cx));
+        assert!(
+            can_trust_dir,
+            "directory worktree should be trusted after its parent is trusted"
+        );
+        assert!(
+            can_trust_file_after,
+            "single-file worktree should be trusted after directory worktree trust via its parent directory trust"
+        );
+    }
+
     #[gpui::test]
     async fn test_abs_path_trust_covers_multiple_worktrees(cx: &mut TestAppContext) {
         init_test(cx);