③ 资源画布布局的读/写命令补项目权限门
- read_local_project_resource_canvas_layout 补 enforce_project_auto_permission_policy(root, "asset.list"),与紧邻的 read_local_project_resource_graph 完全同口径 - update_local_project_resource_canvas_layout 补 enforce_project_permission_policy(root, "asset.register"),与本文件 update_local_project_resource_classification / delete_local_project_asset / rename_local_project_asset 同口径 - 权限位依据:读路径是同一块资源画布的渲染读路径,邻居用 asset.list;写路径是「改动项目内资源相关持久化数据」,manifest 侧的分类写入本身就用 acquire_project_write_lock(root, "asset.register"),故沿用同一权限位,不新造权限名 - 读用 auto、写用普通门是刻意的:asset.list 是 Auto,auto 口径默认放行,只有策略显式 deny/confirm 才拒绝(读路径没有可插入的确认交互,要求确认等同拒绝);asset.register 默认是 Confirm,若写路径也用 auto 会直接打死默认路径 - tests/project.rs 补断言:默认策略下读与写都放行;deny asset.list 读失败、confirm asset.list 也读失败(钉住 auto 口径这一选择);deny asset.register 时读仍放行、写被拒且被拒的写不留任何落盘副作用(断言 sidecar revision 未变);恢复默认策略后写入照旧成功
This commit is contained in:
@@ -935,12 +935,21 @@ pub(crate) fn get_local_game_manifest(
|
||||
read_manifest_for_project_with_godot_root_calibration(root)
|
||||
}
|
||||
|
||||
/// 读取资源画布的持久化布局(`.agent/workbench/resource-layouts/*.json`)。
|
||||
///
|
||||
/// 权限门与紧邻的 [`read_local_project_resource_graph`] 同口径:同一块资源画布、同一条渲染读路径,
|
||||
/// 用的都是 `asset.list` 的 auto 口径。`asset.list` 在 `GAME_CREATION_APP_COMMANDS` 里是
|
||||
/// `GameCreationAppPermission::Auto`,auto 口径默认就放行;只有项目策略显式把 `asset.list` 写进
|
||||
/// `denyCommands` 或 `confirmCommands` 才拒绝。这里必须用 auto 而不是普通读门,是因为这条读路径
|
||||
/// 由画布装载直接触发、没有可插入的确认交互——要求确认等同于拒绝,而普通读门会静默放行。
|
||||
#[tauri::command]
|
||||
pub(crate) fn read_local_project_resource_canvas_layout(
|
||||
project_path: String,
|
||||
mode: ProjectResourceCanvasLayoutMode,
|
||||
) -> Result<ProjectResourceCanvasLayout, String> {
|
||||
read_project_resource_canvas_layout_at(Path::new(project_path.trim()), mode)
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_auto_permission_policy(root, "asset.list")?;
|
||||
read_project_resource_canvas_layout_at(root, mode)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -954,6 +963,15 @@ pub(crate) fn read_local_project_resource_graph(
|
||||
read_project_resource_graph_at(&root, expected_project_id.trim(), resources)
|
||||
}
|
||||
|
||||
/// 保存资源画布的持久化布局。
|
||||
///
|
||||
/// 权限位取 `asset.register`,依据是同批「改动项目内资源相关持久化数据」的既有用法:本文件里
|
||||
/// [`update_local_project_resource_classification`]、[`delete_local_project_asset`]、
|
||||
/// [`rename_local_project_asset`] 全部用 `enforce_project_permission_policy(root, "asset.register")`,
|
||||
/// 被改写的分类写入本身也用 `acquire_project_write_lock(root, "asset.register")`(见
|
||||
/// `project/manifest.rs` 的 `update_manifest_asset_classification_at`)。这里刻意用普通写门而不是
|
||||
/// auto 口径:`asset.register` 默认为 `Confirm`,auto 口径会把它当成「必须确认却无人确认」而拒绝,
|
||||
/// 直接打死默认路径;拖动排版本来就是可以在 UI 里弹确认的用户动作。
|
||||
#[tauri::command]
|
||||
pub(crate) fn update_local_project_resource_canvas_layout(
|
||||
project_path: String,
|
||||
@@ -962,8 +980,10 @@ pub(crate) fn update_local_project_resource_canvas_layout(
|
||||
expected_revision: u64,
|
||||
positions: Vec<ProjectResourceCanvasPosition>,
|
||||
) -> Result<UpdateProjectResourceCanvasLayoutResult, String> {
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "asset.register")?;
|
||||
update_project_resource_canvas_layout_at(
|
||||
Path::new(project_path.trim()),
|
||||
root,
|
||||
mode,
|
||||
&expected_project_id,
|
||||
expected_revision,
|
||||
|
||||
@@ -2856,6 +2856,136 @@ fn local_game_manifest_reads_respect_declared_command_policy() {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
fn resource_canvas_layout_position(resource_id: &str) -> ProjectResourceCanvasPosition {
|
||||
ProjectResourceCanvasPosition {
|
||||
resource_id: resource_id.to_string(),
|
||||
section: shared_contracts::game_creation_app::ProjectResourceCanvasSection::Scene,
|
||||
x: 32,
|
||||
y: 48,
|
||||
manually_placed: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// 资源画布布局的读写命令必须和紧邻的 `read_local_project_resource_graph` 一样过项目权限策略:
|
||||
/// 读用 `asset.list` 的 auto 口径,写用 `asset.register` 的普通写门(与
|
||||
/// `update_local_project_resource_classification` 同口径)。assertions 同时钉住两条口径的区别,
|
||||
/// 以及「被拒绝的写不产生任何落盘副作用」。
|
||||
#[test]
|
||||
fn local_project_resource_canvas_layout_commands_respect_declared_command_policy() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
||||
let project_path = root.to_string_lossy().into_owned();
|
||||
let layout_path = root.join(".agent/workbench/resource-layouts/type.json");
|
||||
|
||||
// 默认策略:`asset.list` 是 auto、`asset.register` 是 confirm,读与写都必须照旧放行。
|
||||
let layout = read_local_project_resource_canvas_layout(
|
||||
project_path.clone(),
|
||||
ProjectResourceCanvasLayoutMode::Type,
|
||||
)
|
||||
.expect("default policy must allow the layout read");
|
||||
assert_eq!(layout.revision, 0);
|
||||
update_local_project_resource_canvas_layout(
|
||||
project_path.clone(),
|
||||
"project-1".to_string(),
|
||||
ProjectResourceCanvasLayoutMode::Type,
|
||||
0,
|
||||
vec![resource_canvas_layout_position("asset-hero")],
|
||||
)
|
||||
.expect("default policy must allow the layout write");
|
||||
assert!(layout_path.is_file(), "默认策略下布局应已落盘");
|
||||
|
||||
write_project_permission_policy_at(
|
||||
&root,
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: vec!["asset.list".to_string()],
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write denied read policy");
|
||||
|
||||
let read_error = read_local_project_resource_canvas_layout(
|
||||
project_path.clone(),
|
||||
ProjectResourceCanvasLayoutMode::Type,
|
||||
)
|
||||
.expect_err("denied asset.list must reject the layout read");
|
||||
assert!(
|
||||
read_error.contains("项目权限策略拒绝执行:asset.list"),
|
||||
"unexpected error: {read_error}"
|
||||
);
|
||||
|
||||
// 读路径没有可插入的确认交互,所以 auto 口径下「要求确认」等同于拒绝。
|
||||
write_project_permission_policy_at(
|
||||
&root,
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: vec!["asset.list".to_string()],
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write confirm read policy");
|
||||
|
||||
let confirm_error = read_local_project_resource_canvas_layout(
|
||||
project_path.clone(),
|
||||
ProjectResourceCanvasLayoutMode::Type,
|
||||
)
|
||||
.expect_err("confirm-required asset.list must reject the layout read");
|
||||
assert!(
|
||||
confirm_error.contains("项目权限策略要求用户确认:asset.list"),
|
||||
"unexpected error: {confirm_error}"
|
||||
);
|
||||
|
||||
write_project_permission_policy_at(
|
||||
&root,
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: vec!["asset.register".to_string()],
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write denied write policy");
|
||||
|
||||
read_local_project_resource_canvas_layout(
|
||||
project_path.clone(),
|
||||
ProjectResourceCanvasLayoutMode::Type,
|
||||
)
|
||||
.expect("only asset.register is denied, so the layout read must still succeed");
|
||||
let write_error = update_local_project_resource_canvas_layout(
|
||||
project_path.clone(),
|
||||
"project-1".to_string(),
|
||||
ProjectResourceCanvasLayoutMode::Type,
|
||||
1,
|
||||
vec![resource_canvas_layout_position("asset-hero")],
|
||||
)
|
||||
.expect_err("denied asset.register must reject the layout write");
|
||||
assert!(
|
||||
write_error.contains("项目权限策略拒绝执行:asset.register"),
|
||||
"unexpected error: {write_error}"
|
||||
);
|
||||
let persisted: ProjectResourceCanvasLayout =
|
||||
serde_json::from_str(&fs::read_to_string(&layout_path).expect("read persisted layout"))
|
||||
.expect("parse persisted layout");
|
||||
assert_eq!(
|
||||
persisted.revision, 1,
|
||||
"被权限门拒绝的写入不得留下任何落盘副作用"
|
||||
);
|
||||
|
||||
// 恢复默认策略后写入照旧成功:新增的门只在策略显式拒绝时生效。
|
||||
write_project_permission_policy_at(&root, ProjectPermissionPolicy::default())
|
||||
.expect("reset policy");
|
||||
let updated = update_local_project_resource_canvas_layout(
|
||||
project_path,
|
||||
"project-1".to_string(),
|
||||
ProjectResourceCanvasLayoutMode::Type,
|
||||
1,
|
||||
vec![resource_canvas_layout_position("asset-hero")],
|
||||
)
|
||||
.expect("default policy must allow the layout write again");
|
||||
assert_eq!(updated.layout.revision, 2);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_project_file_commands_reject_unsafe_paths() {
|
||||
let root = unique_project_path();
|
||||
|
||||
Reference in New Issue
Block a user