免除完美像素已鉴权来源的全账号扫描
来源资源已随项目完成鉴权时直接取用其 objectKey 以显式归属断言替代跨记录扫描并保留存储类型点查 补齐免查解析的一致性与归属回归测试 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4518,17 +4518,79 @@ fn push_editor_pixel_art_source_asset_kind(
|
||||
}
|
||||
}
|
||||
|
||||
// 中文注释:source_resource 来自 owner-scoped 的 get_editor_project,已经完成鉴权,
|
||||
// 因此可以直接用它的 objectKey,不必再按注册 ID 做两轮全账号项目与素材库扫描。做法
|
||||
// 对齐图集拆分:用对资源的显式归属断言替代扫描,而不是省掉校验。
|
||||
// 前提是 sourceImageSrc 能在不发 RPC 的前提下确认指向同一张图——它要么本身就是
|
||||
// objectKey,要么就是这个 resourceId。否则落回完整解析路径。
|
||||
fn resolve_editor_pixel_art_source_without_lookup(
|
||||
owner_user_id: &str,
|
||||
project_id: &str,
|
||||
source: &str,
|
||||
source_resource: &EditorProjectResourcePayload,
|
||||
) -> Result<Option<String>, AppError> {
|
||||
if source_resource.owner_user_id != owner_user_id || source_resource.project_id != project_id {
|
||||
return Err(editor_pixel_art_snap_failure(
|
||||
StatusCode::FORBIDDEN,
|
||||
"来源项目资源不属于当前账号或当前项目。",
|
||||
));
|
||||
}
|
||||
let source_resource_object_key = normalize_editor_record_object_key(
|
||||
source_resource.object_key.as_deref(),
|
||||
source_resource.image_src.as_str(),
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
editor_pixel_art_snap_failure(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"来源项目资源没有可用的稳定图片引用。",
|
||||
)
|
||||
})?;
|
||||
let source = source.trim();
|
||||
if source == source_resource.resource_id.trim() {
|
||||
return Ok(Some(source_resource_object_key));
|
||||
}
|
||||
match normalize_editor_reference_object_key(source) {
|
||||
// 中文注释:sourceImageSrc 已是 objectKey,直接比对即可判定一致性;不一致必须
|
||||
// 报错而不是落回慢路径,否则「两个字段指向不同图片」会被慢路径静默接受。
|
||||
Ok(source_object_key) => {
|
||||
if source_object_key != source_resource_object_key {
|
||||
return Err(editor_pixel_art_snap_failure(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"sourceImageSrc 与 sourceResourceId 指向不同图片。",
|
||||
));
|
||||
}
|
||||
Ok(Some(source_resource_object_key))
|
||||
}
|
||||
// sourceImageSrc 是别的注册 ID,只能走完整解析。
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_editor_pixel_art_source_for_owner(
|
||||
state: &AppState,
|
||||
owner_user_id: &str,
|
||||
project_id: &str,
|
||||
source: &str,
|
||||
project: &EditorProjectPayload,
|
||||
source_resource: Option<&EditorProjectResourcePayload>,
|
||||
requested_asset_kind: Option<&str>,
|
||||
) -> Result<EditorPixelArtSourceResolution, AppError> {
|
||||
let object_key =
|
||||
resolve_editor_reference_object_key_for_owner(state, owner_user_id, source).await?;
|
||||
if let Some(source_resource) = source_resource {
|
||||
let resolved_without_lookup = match source_resource {
|
||||
Some(source_resource) => resolve_editor_pixel_art_source_without_lookup(
|
||||
owner_user_id,
|
||||
project_id,
|
||||
source,
|
||||
source_resource,
|
||||
)?,
|
||||
None => None,
|
||||
};
|
||||
let object_key = match resolved_without_lookup.clone() {
|
||||
Some(object_key) => object_key,
|
||||
None => resolve_editor_reference_object_key_for_owner(state, owner_user_id, source).await?,
|
||||
};
|
||||
if resolved_without_lookup.is_none()
|
||||
&& let Some(source_resource) = source_resource
|
||||
{
|
||||
let source_resource_object_key = normalize_editor_record_object_key(
|
||||
source_resource.object_key.as_deref(),
|
||||
source_resource.image_src.as_str(),
|
||||
@@ -4561,44 +4623,52 @@ async fn resolve_editor_pixel_art_source_for_owner(
|
||||
resource.asset_kind.as_deref(),
|
||||
);
|
||||
}
|
||||
let projects = state
|
||||
.spacetime_client()
|
||||
.list_editor_projects(owner_user_id.to_string())
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
for resource in projects
|
||||
.iter()
|
||||
.flat_map(|project| project.resources.iter())
|
||||
.filter(|resource| {
|
||||
// 中文注释:跨记录扫描只在没有已鉴权 source_resource 时才需要。它的作用是防止同一
|
||||
// objectKey 在别处登记为动画/音视频时被静态请求掩盖;有 source_resource 时该语义
|
||||
// 权威已经确定,当前项目内的同键记录仍在上面按内存扫过。字节级门禁
|
||||
// (validate_editor_pixel_art_static_raster)在下载后照常执行,不受此影响。
|
||||
if resolved_without_lookup.is_none() {
|
||||
let projects = state
|
||||
.spacetime_client()
|
||||
.list_editor_projects(owner_user_id.to_string())
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
for resource in projects
|
||||
.iter()
|
||||
.flat_map(|project| project.resources.iter())
|
||||
.filter(|resource| {
|
||||
editor_record_object_key_matches(
|
||||
resource.object_key.as_deref(),
|
||||
resource.image_src.as_str(),
|
||||
object_key.as_str(),
|
||||
)
|
||||
})
|
||||
{
|
||||
push_editor_pixel_art_source_asset_kind(
|
||||
&mut discovered_asset_kinds,
|
||||
resource.asset_kind.as_deref(),
|
||||
);
|
||||
}
|
||||
let library = state
|
||||
.spacetime_client()
|
||||
.get_editor_asset_library(owner_user_id.to_string(), current_utc_micros())
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
for asset in library.assets.iter().filter(|asset| {
|
||||
editor_record_object_key_matches(
|
||||
resource.object_key.as_deref(),
|
||||
resource.image_src.as_str(),
|
||||
asset.object_key.as_deref(),
|
||||
asset.image_src.as_str(),
|
||||
object_key.as_str(),
|
||||
)
|
||||
})
|
||||
{
|
||||
push_editor_pixel_art_source_asset_kind(
|
||||
&mut discovered_asset_kinds,
|
||||
resource.asset_kind.as_deref(),
|
||||
);
|
||||
}
|
||||
let library = state
|
||||
.spacetime_client()
|
||||
.get_editor_asset_library(owner_user_id.to_string(), current_utc_micros())
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
for asset in library.assets.iter().filter(|asset| {
|
||||
editor_record_object_key_matches(
|
||||
asset.object_key.as_deref(),
|
||||
asset.image_src.as_str(),
|
||||
object_key.as_str(),
|
||||
)
|
||||
}) {
|
||||
push_editor_pixel_art_source_asset_kind(
|
||||
&mut discovered_asset_kinds,
|
||||
asset.asset_kind.as_deref(),
|
||||
);
|
||||
}) {
|
||||
push_editor_pixel_art_source_asset_kind(
|
||||
&mut discovered_asset_kinds,
|
||||
asset.asset_kind.as_deref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
// 中文注释:这一处是按 (bucket, objectKey) 的点查而不是全账号扫描,成本与账号规模
|
||||
// 无关,两条路径都保留——它承担存储 taxonomy 的非静态门禁。
|
||||
if let Some(oss_client) = state.oss_client()
|
||||
&& let Some(asset_object) = state
|
||||
.spacetime_client()
|
||||
@@ -4706,6 +4776,7 @@ pub async fn snap_editor_image_to_pixel_art(
|
||||
let source = resolve_editor_pixel_art_source_for_owner(
|
||||
&state,
|
||||
owner_user_id.as_str(),
|
||||
project_id.as_str(),
|
||||
payload.source_image_src.as_str(),
|
||||
&project,
|
||||
source_resource,
|
||||
@@ -10317,6 +10388,75 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pixel_art_authorized_source_resource_skips_account_wide_lookup() {
|
||||
let resource = editor_project_resource_for_canvas_test("res-1", "character", 128, 128);
|
||||
let object_key = "generated-character-drafts/editor/res-1.png";
|
||||
|
||||
// sourceImageSrc 就是该 resourceId:无需任何 RPC 即可定位。
|
||||
assert_eq!(
|
||||
resolve_editor_pixel_art_source_without_lookup(
|
||||
"user-1",
|
||||
"editor-project-1",
|
||||
"res-1",
|
||||
&resource,
|
||||
)
|
||||
.expect("authorized resource should resolve"),
|
||||
Some(object_key.to_string())
|
||||
);
|
||||
|
||||
// sourceImageSrc 已是同一个 objectKey:同样免 RPC。
|
||||
assert_eq!(
|
||||
resolve_editor_pixel_art_source_without_lookup(
|
||||
"user-1",
|
||||
"editor-project-1",
|
||||
object_key,
|
||||
&resource,
|
||||
)
|
||||
.expect("matching object key should resolve"),
|
||||
Some(object_key.to_string())
|
||||
);
|
||||
|
||||
// 中文注释:两个字段指向不同图片必须直接报错,不能落回慢路径——慢路径会用
|
||||
// sourceImageSrc 解析出的 key 继续,等于让 sourceResourceId 形同虚设。
|
||||
let mismatched = resolve_editor_pixel_art_source_without_lookup(
|
||||
"user-1",
|
||||
"editor-project-1",
|
||||
"generated-character-drafts/editor/other.png",
|
||||
&resource,
|
||||
)
|
||||
.expect_err("mismatched object key must fail");
|
||||
assert_eq!(mismatched.status_code(), StatusCode::BAD_REQUEST);
|
||||
|
||||
// sourceImageSrc 是别的注册 ID:无法免 RPC 判定,落回完整解析。
|
||||
assert_eq!(
|
||||
resolve_editor_pixel_art_source_without_lookup(
|
||||
"user-1",
|
||||
"editor-project-1",
|
||||
"editor-asset-42",
|
||||
&resource,
|
||||
)
|
||||
.expect("unknown registered id should fall back"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pixel_art_source_resource_ownership_is_asserted_before_skipping_lookup() {
|
||||
let resource = editor_project_resource_for_canvas_test("res-1", "character", 128, 128);
|
||||
|
||||
// 中文注释:跳过全账号扫描的前提是这条断言,不能省。
|
||||
for (owner, project) in [
|
||||
("other-user", "editor-project-1"),
|
||||
("user-1", "other-project"),
|
||||
] {
|
||||
let error =
|
||||
resolve_editor_pixel_art_source_without_lookup(owner, project, "res-1", &resource)
|
||||
.expect_err("foreign resource must be rejected");
|
||||
assert_eq!(error.status_code(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_project_procedure_errors_preserve_business_reason_as_bad_request() {
|
||||
let error = map_editor_project_error(SpacetimeClientError::Procedure(
|
||||
|
||||
Reference in New Issue
Block a user