дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
fix(trust): allow application-level commands in restricted mode (#648)
Help › About showed no version, build or runtime in untrusted project windows and on the welcome screen. Root cause: the restricted-mode allowlist added with project trust missed app_build_info and other application-level commands, so they were rejected wherever the window had no trusted project; the frontend then cached the failed build info for the whole session. Fix: classify all registered commands and allow the 24 that only read application information or change the user's own application state (build info, log location, update checks, cache invalidation, file clipboard, JDK download, Docker settings, database export paths, patch export, system environment); JDK download now uses the application tool directory instead of project tool settings. Execution commands (run, build, debug, language servers, tool install from project settings) stay blocked. Build info is retried after a failure instead of being cached. Verified on macOS in an untrusted project and on the welcome screen; Windows not run. Related to #519
Цей коміт міститься в:
1 джерело
376501a5fe
коміт
7c72411dd7
5 files changed
+173
-5
No files matched your search
@@ -100,11 +100,11 @@ pub fn jdk_download_vendors(_host: &Host, _window: &str) -> Vec<JdkVendor> {
|
||||
/// Java versions `vendor` publishes; cancellable with `cancel_request(request)`.
|
||||
pub async fn jdk_download_releases(
|
||||
host: &Host,
|
||||
window: &str,
|
||||
_window: &str,
|
||||
vendor: String,
|
||||
request: RequestId,
|
||||
) -> IdeResult<JdkReleases> {
|
||||
let tools = tools(host, window)?;
|
||||
let tools = Tools::new(&host.data_dir);
|
||||
let cancel = host.begin_request(request);
|
||||
let result = tokio::task::spawn_blocking(move || tools.jdk_releases(&vendor, &cancel)).await.map_err(join_err);
|
||||
host.end_request(request);
|
||||
@@ -123,14 +123,14 @@ pub fn jdk_download_location(host: &Host, _window: &str) -> IdeResult<ide_core::
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn jdk_download(
|
||||
host: &Host,
|
||||
window: &str,
|
||||
_window: &str,
|
||||
vendor: String,
|
||||
version: u32,
|
||||
location: Option<PathBuf>,
|
||||
request: RequestId,
|
||||
on_event: Channel<InstallEvent>,
|
||||
) -> IdeResult<Sdk> {
|
||||
let tools = tools(host, window)?;
|
||||
let tools = Tools::new(&host.data_dir);
|
||||
let location = location.filter(|p| !p.as_os_str().is_empty());
|
||||
let cancel = host.begin_request(request);
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
|
||||
@@ -175,6 +175,30 @@ pub fn restricted_command_allowed(cmd: &str) -> bool {
|
||||
matches!(
|
||||
cmd,
|
||||
"app_info"
|
||||
| "app_build_info"
|
||||
| "app_log_location"
|
||||
| "app_check_update"
|
||||
| "app_startup_update_check"
|
||||
| "app_install_update"
|
||||
| "caches_schedule_invalidation"
|
||||
| "clear_caches"
|
||||
| "clipboard_file_list"
|
||||
| "clipboard_has_files"
|
||||
| "clipboard_set_file_list"
|
||||
| "jdk_download_vendors"
|
||||
| "jdk_download_releases"
|
||||
| "jdk_download_location"
|
||||
| "jdk_download"
|
||||
| "run_system_env"
|
||||
| "run_gradle_jvm_args_script"
|
||||
| "proxy_check_connection"
|
||||
| "docker_settings"
|
||||
| "docker_save_settings"
|
||||
| "docker_set_registry_password"
|
||||
| "docker_run_env"
|
||||
| "db_console_path"
|
||||
| "db_export_file"
|
||||
| "vcs_write_patch"
|
||||
| "app_metrics"
|
||||
| "app_quit"
|
||||
| "app_cancel_quit"
|
||||
|
||||
@@ -20,6 +20,94 @@ fn host(base: &Path, environment: bool) -> Arc<Host> {
|
||||
}))
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn application_commands_work_in_restricted_projects_and_without_a_project() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path().join("project");
|
||||
std::fs::create_dir(&root).unwrap();
|
||||
let host = host(&tmp.path().join("app"), false);
|
||||
assert!(host.project("welcome").is_err());
|
||||
assert!(ide_host::ops::project::roots(&host, "welcome").is_err());
|
||||
assert!(!host.open_project("main", &root).unwrap().is_trusted());
|
||||
for command in [
|
||||
"app_info",
|
||||
"app_build_info",
|
||||
"app_log_location",
|
||||
"app_check_update",
|
||||
"app_startup_update_check",
|
||||
"app_install_update",
|
||||
"caches_schedule_invalidation",
|
||||
"clear_caches",
|
||||
"clipboard_file_list",
|
||||
"clipboard_has_files",
|
||||
"clipboard_set_file_list",
|
||||
"jdk_download_vendors",
|
||||
"jdk_download_releases",
|
||||
"jdk_download_location",
|
||||
"jdk_download",
|
||||
"run_system_env",
|
||||
"run_gradle_jvm_args_script",
|
||||
"proxy_check_connection",
|
||||
"docker_settings",
|
||||
"docker_save_settings",
|
||||
"docker_set_registry_password",
|
||||
"docker_run_env",
|
||||
"db_console_path",
|
||||
"db_export_file",
|
||||
"vcs_write_patch",
|
||||
] {
|
||||
assert!(ide_host::trust::restricted_command_allowed(command), "{command}");
|
||||
for window in ["main", "welcome"] {
|
||||
assert!(host.authorize_command(window, command).is_ok(), "{window}: {command}");
|
||||
}
|
||||
}
|
||||
for command in [
|
||||
"run_start",
|
||||
"build_sync",
|
||||
"debug_start",
|
||||
"sdk_detect",
|
||||
"tools_list",
|
||||
"tools_install",
|
||||
"tools_uninstall",
|
||||
"plugin_install_dir",
|
||||
"plugin_reload",
|
||||
"vcs_git_version",
|
||||
"docker_compose_op",
|
||||
"db_execute",
|
||||
"unknown_command",
|
||||
] {
|
||||
assert!(!ide_host::trust::restricted_command_allowed(command), "{command}");
|
||||
for window in ["main", "welcome"] {
|
||||
assert!(host.authorize_command(window, command).is_err(), "{window}: {command}");
|
||||
}
|
||||
}
|
||||
// These requests validate the vendor before any network access, independently of project trust.
|
||||
for window in ["main", "welcome"] {
|
||||
let error = ide_host::ops::tools::jdk_download_releases(
|
||||
&host,
|
||||
window,
|
||||
"unknown-vendor".into(),
|
||||
ide_core::RequestId(901),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(error.kind, ide_core::error::ErrorKind::InvalidInput);
|
||||
let error = ide_host::ops::tools::jdk_download(
|
||||
&host,
|
||||
window,
|
||||
"unknown-vendor".into(),
|
||||
25,
|
||||
None,
|
||||
ide_core::RequestId(902),
|
||||
ide_host::ops::Channel::new(|_| {}),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(error.kind, ide_core::error::ErrorKind::InvalidInput);
|
||||
}
|
||||
host.shutdown().await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persists_user_decisions_locations_revocation_and_one_time_migration() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { beforeEach, expect, it, vi } from "vitest";
|
||||
import type { BuildInfo } from "@/platform/ipc";
|
||||
|
||||
const { appBuildInfo, isTauri } = vi.hoisted(() => ({ appBuildInfo: vi.fn(), isTauri: vi.fn() }));
|
||||
vi.mock("@/platform/ipc", () => ({ commands: { appBuildInfo }, isTauri }));
|
||||
|
||||
const build: BuildInfo = {
|
||||
version: "0.1.32",
|
||||
build: "dev",
|
||||
profile: "debug",
|
||||
os: "windows",
|
||||
arch: "x86_64",
|
||||
tauri: "2.11.3",
|
||||
updates: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
appBuildInfo.mockReset();
|
||||
isTauri.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("retries after a rejected request and caches the successful build info", async () => {
|
||||
appBuildInfo.mockRejectedValueOnce(new Error("Project is not trusted")).mockResolvedValueOnce(build);
|
||||
const { buildInfo, hostOs } = await import("../host");
|
||||
|
||||
const first = buildInfo();
|
||||
expect(buildInfo()).toBe(first);
|
||||
await expect(first).resolves.toBeNull();
|
||||
await expect(hostOs()).resolves.toBe("windows");
|
||||
await expect(buildInfo()).resolves.toBe(build);
|
||||
expect(appBuildInfo).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("shares an in-flight successful request", async () => {
|
||||
let resolve!: (value: BuildInfo) => void;
|
||||
appBuildInfo.mockReturnValue(new Promise<BuildInfo>((done) => (resolve = done)));
|
||||
const { buildInfo } = await import("../host");
|
||||
const first = buildInfo();
|
||||
expect(buildInfo()).toBe(first);
|
||||
expect(appBuildInfo).toHaveBeenCalledTimes(1);
|
||||
resolve(build);
|
||||
await expect(first).resolves.toBe(build);
|
||||
});
|
||||
|
||||
it("does not invoke the backend outside Tauri", async () => {
|
||||
isTauri.mockReturnValue(false);
|
||||
const { buildInfo } = await import("../host");
|
||||
await expect(buildInfo()).resolves.toBeNull();
|
||||
expect(appBuildInfo).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -13,7 +13,12 @@ function normalize(os: string): HostOs {
|
||||
let info: Promise<BuildInfo | null> | null = null;
|
||||
|
||||
export function buildInfo(): Promise<BuildInfo | null> {
|
||||
info ??= isTauri() ? commands.appBuildInfo().catch(() => null) : Promise.resolve(null);
|
||||
info ??= isTauri()
|
||||
? commands.appBuildInfo().catch(() => {
|
||||
info = null;
|
||||
return null;
|
||||
})
|
||||
: Promise.resolve(null);
|
||||
return info;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Заблокувати користувача