forked from wangziqi/gongxue-base
feat: harden content asset access
This commit is contained in:
@@ -3148,11 +3148,29 @@ async function testTenantContentAssetsAndImports() {
|
||||
regionId: ids.region,
|
||||
subjectId: ids.subject,
|
||||
categoryId: ids.category,
|
||||
metadata: { source: 'api-integration' },
|
||||
metadata: { source: 'api-integration', providerManagedAccess: true },
|
||||
},
|
||||
});
|
||||
assert.equal(asset.item?.visibility, 'svip', 'tenant admin should create svip asset');
|
||||
|
||||
const lockedCdnAsset = await request('/api/tenant-content/assets', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
title: '集成测试未授权 CDN SVIP 资料',
|
||||
assetType: 'pdf',
|
||||
storageProvider: 'external_url',
|
||||
cdnUrl: 'https://example.test/resources/locked-without-provider-managed.pdf',
|
||||
fileName: 'locked-without-provider-managed.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
visibility: 'svip',
|
||||
regionId: ids.region,
|
||||
subjectId: ids.subject,
|
||||
categoryId: ids.category,
|
||||
metadata: { source: 'api-integration' },
|
||||
},
|
||||
});
|
||||
|
||||
const adminAssets = await request('/api/tenant-content/assets', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { assetType: 'pdf', regionId: ids.region },
|
||||
@@ -3178,6 +3196,31 @@ async function testTenantContentAssetsAndImports() {
|
||||
assert.equal(download.access?.svip, true, 'asset download should report svip access');
|
||||
assert.equal(download.download?.url, 'https://example.test/resources/integration.pdf', 'external asset download should use cdn url');
|
||||
assert.equal(download.download?.signatureMode, 'public-or-provider-managed', 'external asset should be marked provider managed');
|
||||
assert.equal(download.download?.expiresInSec, 300, 'locked student asset downloads should use short TTL');
|
||||
|
||||
const deniedLockedCdn = await request('/api/catalog/assets/download', {
|
||||
query: { assetId: lockedCdnAsset.item.id },
|
||||
expectStatus: 409,
|
||||
});
|
||||
assert.equal(deniedLockedCdn.code, 'ASSET_CDN_ACCESS_NOT_ALLOWED', 'locked cdn asset should require explicit provider-managed access');
|
||||
|
||||
const accessEventsAfterLockedDeny = await request('/api/tenant-content/assets/access-events', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { assetId: lockedCdnAsset.item.id },
|
||||
});
|
||||
assert.ok(
|
||||
accessEventsAfterLockedDeny.items?.some(item => item.result === 'denied' && item.denyCode === 'ASSET_CDN_ACCESS_NOT_ALLOWED'),
|
||||
'locked cdn denial should be recorded as an asset access event',
|
||||
);
|
||||
|
||||
const accessEventsAfterDownload = await request('/api/tenant-content/assets/access-events', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { assetId: asset.item.id },
|
||||
});
|
||||
assert.ok(
|
||||
accessEventsAfterDownload.items?.some(item => item.accessType === 'download' && item.result === 'granted'),
|
||||
'successful student asset download should be recorded as an access event',
|
||||
);
|
||||
|
||||
const localAsset = await request('/api/tenant-content/assets', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
@@ -3233,6 +3276,7 @@ async function testTenantContentAssetsAndImports() {
|
||||
assert.equal(localPreview.preview?.method, 'GET', 'asset preview should sign GET');
|
||||
assert.equal(localPreview.preview?.signatureMode, 'local-placeholder', 'local asset preview should use local placeholder signer');
|
||||
assert.ok(localPreview.preview?.url?.includes('disposition=inline'), 'asset preview should request inline disposition');
|
||||
assert.equal(localPreview.preview?.expiresInSec, 300, 'student inline previews should use short TTL');
|
||||
|
||||
const adminPreview = await request('/api/tenant-content/assets/sign-preview', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
@@ -3241,6 +3285,15 @@ async function testTenantContentAssetsAndImports() {
|
||||
});
|
||||
assert.ok(adminPreview.preview?.url?.includes('disposition=inline'), 'admin preview should request inline disposition');
|
||||
|
||||
const localAccessEvents = await request('/api/tenant-content/assets/access-events', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { assetId: localAsset.item.id },
|
||||
});
|
||||
assert.ok(
|
||||
localAccessEvents.items?.some(item => item.accessType === 'preview' && item.expiresInSec === 300),
|
||||
'student preview access event should include short TTL',
|
||||
);
|
||||
|
||||
const failedAsset = await request('/api/tenant-content/assets', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
|
||||
@@ -1354,22 +1354,30 @@ async function main() {
|
||||
`
|
||||
insert into public.content_assets (
|
||||
id, tenant_id, asset_key, title, asset_type, storage_provider,
|
||||
bucket, object_key, file_name, mime_type, visibility, status, source
|
||||
bucket, object_key, file_name, mime_type, file_size_bytes,
|
||||
checksum_sha256, visibility, status, upload_status, verified_at,
|
||||
verified_size_bytes, verified_checksum_sha256, source
|
||||
)
|
||||
values (
|
||||
$1, $2, 'smoke-video-asset', '烟测视频对象', 'video', 'local_dev',
|
||||
'tenant-assets', $3, 'smoke.mp4', 'video/mp4',
|
||||
'svip', 'active', 'smoke-seed'
|
||||
'tenant-assets', $3, 'smoke.mp4', 'video/mp4', 8192,
|
||||
$4, 'svip', 'active', 'verified', now(), 8192, $4, 'smoke-seed'
|
||||
)
|
||||
on conflict (id)
|
||||
do update set storage_provider = excluded.storage_provider,
|
||||
bucket = excluded.bucket,
|
||||
object_key = excluded.object_key,
|
||||
file_size_bytes = excluded.file_size_bytes,
|
||||
checksum_sha256 = excluded.checksum_sha256,
|
||||
visibility = excluded.visibility,
|
||||
status = 'active',
|
||||
upload_status = 'verified',
|
||||
verified_at = coalesce(public.content_assets.verified_at, now()),
|
||||
verified_size_bytes = excluded.verified_size_bytes,
|
||||
verified_checksum_sha256 = excluded.verified_checksum_sha256,
|
||||
updated_at = now()
|
||||
`,
|
||||
[ids.videoAsset, tenantId, `${tenantId}/videos/smoke.mp4`],
|
||||
[ids.videoAsset, tenantId, `${tenantId}/videos/smoke.mp4`, 'c'.repeat(64)],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
|
||||
Reference in New Issue
Block a user