修复公开资产授权一致性与签名期限
公开资产 metadata 和作品授权改为在同一 SpacetimeDB 事务快照中判断 移除连接级公开授权订阅缓存对 ACL 的参与 公开读取签名有效期上限固定为 600 秒并保留 owner 和 admin 行为 同步生成 bindings 并更新后端契约与项目记忆
This commit is contained in:
@@ -50,6 +50,7 @@ const SUPPORTED_ASSET_HISTORY_KINDS: [&str; 9] = [
|
||||
// 中文注释:同源字节读取同时服务图片转 Data URL 与 Match3D 私有 GLB 预览,Rodin GLB 可能明显超过图片上限。
|
||||
const ASSET_READ_BYTES_MAX_SIZE_BYTES: u64 = 120 * 1024 * 1024;
|
||||
const ASSET_READ_BYTES_DEFAULT_EXPIRE_SECONDS: u64 = 300;
|
||||
const PUBLIC_ASSET_READ_MAX_EXPIRE_SECONDS: u64 = 600;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum AssetReadAuthorization {
|
||||
@@ -58,6 +59,12 @@ pub(crate) enum AssetReadAuthorization {
|
||||
Admin,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum AssetReadAccessScope {
|
||||
Public,
|
||||
Privileged,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct AssetReadTarget {
|
||||
object_key: String,
|
||||
@@ -188,12 +195,17 @@ pub(crate) async fn get_asset_read_url_with_query(
|
||||
})?;
|
||||
|
||||
let target = resolve_asset_read_target(&query)?;
|
||||
authorize_asset_read_target(state, oss_client.config_bucket(), &target, &authorization).await?;
|
||||
let access_scope =
|
||||
authorize_asset_read_target(state, oss_client.config_bucket(), &target, &authorization)
|
||||
.await?;
|
||||
|
||||
let signed = oss_client
|
||||
.sign_get_object_url(OssSignedGetObjectUrlRequest {
|
||||
object_key: target.object_key,
|
||||
expire_seconds: query.expire_seconds,
|
||||
expire_seconds: clamp_public_asset_read_expire_seconds(
|
||||
query.expire_seconds,
|
||||
access_scope,
|
||||
),
|
||||
})
|
||||
.map_err(|error| {
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
@@ -298,16 +310,20 @@ pub async fn get_asset_read_bytes(
|
||||
"/api/assets/read-bytes",
|
||||
)
|
||||
.await?;
|
||||
authorize_asset_read_target(&state, oss_client.config_bucket(), &target, &authorization)
|
||||
.await?;
|
||||
let access_scope =
|
||||
authorize_asset_read_target(&state, oss_client.config_bucket(), &target, &authorization)
|
||||
.await?;
|
||||
|
||||
let signed = oss_client
|
||||
.sign_get_object_url(OssSignedGetObjectUrlRequest {
|
||||
object_key: target.object_key,
|
||||
expire_seconds: Some(
|
||||
query
|
||||
.expire_seconds
|
||||
.unwrap_or(ASSET_READ_BYTES_DEFAULT_EXPIRE_SECONDS),
|
||||
expire_seconds: clamp_public_asset_read_expire_seconds(
|
||||
Some(
|
||||
query
|
||||
.expire_seconds
|
||||
.unwrap_or(ASSET_READ_BYTES_DEFAULT_EXPIRE_SECONDS),
|
||||
),
|
||||
access_scope,
|
||||
),
|
||||
})
|
||||
.map_err(|error| map_oss_error(error, "aliyun-oss"))?;
|
||||
@@ -657,32 +673,20 @@ async fn authorize_asset_read_target(
|
||||
configured_bucket: &str,
|
||||
target: &AssetReadTarget,
|
||||
authorization: &AssetReadAuthorization,
|
||||
) -> Result<(), AppError> {
|
||||
) -> Result<AssetReadAccessScope, AppError> {
|
||||
if matches!(authorization, AssetReadAuthorization::Admin) {
|
||||
return Ok(());
|
||||
return Ok(AssetReadAccessScope::Privileged);
|
||||
}
|
||||
|
||||
let asset_object = state
|
||||
let (asset_object, public_work_granted) = state
|
||||
.spacetime_client()
|
||||
.get_asset_object_by_location(module_assets::AssetObjectLocationInput {
|
||||
.get_asset_read_access_by_location(module_assets::AssetObjectLocationInput {
|
||||
bucket: configured_bucket.to_string(),
|
||||
object_key: target.object_key.clone(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_asset_read_authorization_error)?;
|
||||
if let Some(asset_object) = asset_object.as_ref() {
|
||||
let public_work_granted = if asset_object.access_policy == AssetObjectAccessPolicy::Private
|
||||
&& !asset_object_owner_matches(asset_object, authorization)
|
||||
&& asset_object_storage_matches(asset_object, configured_bucket, &target.object_key)
|
||||
{
|
||||
state
|
||||
.spacetime_client()
|
||||
.is_asset_object_referenced_by_public_work(asset_object.clone())
|
||||
.await
|
||||
.map_err(map_asset_read_authorization_error)?
|
||||
} else {
|
||||
false
|
||||
};
|
||||
return require_asset_object_read_access(
|
||||
asset_object,
|
||||
configured_bucket,
|
||||
@@ -694,7 +698,7 @@ async fn authorize_asset_read_target(
|
||||
|
||||
// 已登记对象始终服从 metadata ACL;只有没有 metadata 的历史资源才走公开前缀兼容。
|
||||
if target.is_legacy_public_path && is_supported_legacy_public_object_key(&target.object_key) {
|
||||
return Ok(());
|
||||
return Ok(AssetReadAccessScope::Public);
|
||||
}
|
||||
|
||||
Err(asset_read_not_found())
|
||||
@@ -706,20 +710,34 @@ fn require_asset_object_read_access(
|
||||
object_key: &str,
|
||||
authorization: &AssetReadAuthorization,
|
||||
public_work_granted: bool,
|
||||
) -> Result<(), AppError> {
|
||||
) -> Result<AssetReadAccessScope, AppError> {
|
||||
if !asset_object_storage_matches(asset_object, configured_bucket, object_key) {
|
||||
return Err(asset_read_not_found());
|
||||
}
|
||||
if asset_object.access_policy == AssetObjectAccessPolicy::PublicRead
|
||||
|| public_work_granted
|
||||
|| asset_object_owner_matches(asset_object, authorization)
|
||||
{
|
||||
return Ok(());
|
||||
if asset_object_owner_matches(asset_object, authorization) {
|
||||
return Ok(AssetReadAccessScope::Privileged);
|
||||
}
|
||||
if asset_object.access_policy == AssetObjectAccessPolicy::PublicRead || public_work_granted {
|
||||
return Ok(AssetReadAccessScope::Public);
|
||||
}
|
||||
|
||||
Err(asset_read_not_found())
|
||||
}
|
||||
|
||||
fn clamp_public_asset_read_expire_seconds(
|
||||
requested_expire_seconds: Option<u64>,
|
||||
access_scope: AssetReadAccessScope,
|
||||
) -> Option<u64> {
|
||||
match access_scope {
|
||||
AssetReadAccessScope::Public => Some(
|
||||
requested_expire_seconds
|
||||
.unwrap_or(PUBLIC_ASSET_READ_MAX_EXPIRE_SECONDS)
|
||||
.min(PUBLIC_ASSET_READ_MAX_EXPIRE_SECONDS),
|
||||
),
|
||||
AssetReadAccessScope::Privileged => requested_expire_seconds,
|
||||
}
|
||||
}
|
||||
|
||||
fn asset_object_storage_matches(
|
||||
asset_object: &module_assets::AssetObjectRecord,
|
||||
configured_bucket: &str,
|
||||
@@ -1017,16 +1035,16 @@ mod tests {
|
||||
Some("user-owner"),
|
||||
);
|
||||
|
||||
assert!(
|
||||
assert!(matches!(
|
||||
super::require_asset_object_read_access(
|
||||
&record,
|
||||
"genarrative-assets",
|
||||
record.object_key.as_str(),
|
||||
&super::AssetReadAuthorization::Owner("user-owner".to_string()),
|
||||
false,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
),
|
||||
Ok(super::AssetReadAccessScope::Privileged)
|
||||
));
|
||||
for authorization in [
|
||||
super::AssetReadAuthorization::Anonymous,
|
||||
super::AssetReadAuthorization::Owner("user-other".to_string()),
|
||||
@@ -1047,16 +1065,16 @@ mod tests {
|
||||
fn public_asset_read_allows_anonymous_but_rejects_storage_mismatch() {
|
||||
let record = asset_object_record(module_assets::AssetObjectAccessPolicy::PublicRead, None);
|
||||
|
||||
assert!(
|
||||
assert!(matches!(
|
||||
super::require_asset_object_read_access(
|
||||
&record,
|
||||
"genarrative-assets",
|
||||
record.object_key.as_str(),
|
||||
&super::AssetReadAuthorization::Anonymous,
|
||||
false,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
),
|
||||
Ok(super::AssetReadAccessScope::Public)
|
||||
));
|
||||
assert_eq!(
|
||||
super::require_asset_object_read_access(
|
||||
&record,
|
||||
@@ -1078,16 +1096,16 @@ mod tests {
|
||||
Some("user-owner"),
|
||||
);
|
||||
|
||||
assert!(
|
||||
assert!(matches!(
|
||||
super::require_asset_object_read_access(
|
||||
&record,
|
||||
"genarrative-assets",
|
||||
record.object_key.as_str(),
|
||||
&super::AssetReadAuthorization::Anonymous,
|
||||
true,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
),
|
||||
Ok(super::AssetReadAccessScope::Public)
|
||||
));
|
||||
assert_eq!(
|
||||
super::require_asset_object_read_access(
|
||||
&record,
|
||||
@@ -1102,6 +1120,31 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_asset_read_url_expiry_is_capped_but_privileged_expiry_is_preserved() {
|
||||
assert_eq!(
|
||||
super::clamp_public_asset_read_expire_seconds(
|
||||
None,
|
||||
super::AssetReadAccessScope::Public,
|
||||
),
|
||||
Some(super::PUBLIC_ASSET_READ_MAX_EXPIRE_SECONDS)
|
||||
);
|
||||
assert_eq!(
|
||||
super::clamp_public_asset_read_expire_seconds(
|
||||
Some(86_400),
|
||||
super::AssetReadAccessScope::Public,
|
||||
),
|
||||
Some(super::PUBLIC_ASSET_READ_MAX_EXPIRE_SECONDS)
|
||||
);
|
||||
assert_eq!(
|
||||
super::clamp_public_asset_read_expire_seconds(
|
||||
Some(86_400),
|
||||
super::AssetReadAccessScope::Privileged,
|
||||
),
|
||||
Some(86_400)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_public_fallback_only_accepts_curated_prefixes() {
|
||||
assert!(super::is_supported_legacy_public_object_key(
|
||||
|
||||
Reference in New Issue
Block a user