feat: add object storage signing providers

This commit is contained in:
Codex
2026-06-22 00:58:36 +08:00
parent ef8afcdbc8
commit 492706e24f
13 changed files with 1650 additions and 79 deletions

View File

@@ -11,6 +11,7 @@ const TENANT_OPERATOR_USER_ID = '00000000-0000-0000-0000-000000000103';
const TENANT_SALES_USER_ID = '00000000-0000-0000-0000-000000000104';
const TENANT_AGENT_USER_ID = '00000000-0000-0000-0000-000000000105';
const START_SERVER = process.argv.includes('--start-server');
const ENABLE_REAL_STORAGE_SIGN_TESTS = process.env.ENABLE_REAL_STORAGE_SIGN_TESTS === 'true';
const ids = {
region: '00000000-0000-0000-0000-000000000301',
@@ -633,6 +634,81 @@ async function testTenantContentAssetsAndImports() {
});
assert.equal(upload.assetDraft?.assetType, 'pdf', 'upload signer should return asset draft');
assert.ok(upload.upload?.objectKey?.includes(MAIN_TENANT_ID), 'upload object key should be tenant scoped');
assert.equal(upload.upload?.signatureMode, 'local-placeholder', 'local signer should be explicit in test env');
assert.equal(upload.upload?.headers?.['content-type'], 'application/pdf', 'upload signer should bind content-type');
assert.ok(!JSON.stringify(upload).includes('ACCESS_KEY'), 'upload signer must not leak cloud credentials');
if (ENABLE_REAL_STORAGE_SIGN_TESTS) {
const tencentUpload = await request('/api/tenant-content/assets/sign-upload', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
body: {
fileName: 'cos-resource.pdf',
assetType: 'pdf',
storageProvider: 'tencent_cos',
bucket: process.env.TEST_TENCENT_COS_BUCKET || 'tenant-assets',
objectKey: `${MAIN_TENANT_ID}/pdf/cos-resource.pdf`,
mimeType: 'application/pdf',
},
});
assert.ok(tencentUpload.upload?.url?.includes('q-sign-algorithm=sha1'), 'Tencent COS signed URL should expose XML V5 query params');
assert.ok(!tencentUpload.upload?.url?.includes('SecretKey'), 'Tencent COS signed URL must not leak SecretKey');
}
const unsafeUpload = await request('/api/tenant-content/assets/sign-upload', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
body: {
fileName: 'unsafe.pdf',
assetType: 'pdf',
storageProvider: 'local_dev',
objectKey: '../escape.pdf',
mimeType: 'application/pdf',
},
expectStatus: 400,
});
assert.equal(unsafeUpload.code, 'INVALID_OBJECT_KEY', 'upload signer should reject path traversal object keys');
const crossTenantUpload = await request('/api/tenant-content/assets/sign-upload', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
body: {
fileName: 'cross-tenant.pdf',
assetType: 'pdf',
storageProvider: 'local_dev',
objectKey: `${PARTNER_TENANT_ID}/pdf/cross-tenant.pdf`,
mimeType: 'application/pdf',
},
expectStatus: 400,
});
assert.equal(crossTenantUpload.code, 'OBJECT_KEY_TENANT_PREFIX_REQUIRED', 'upload object key should stay in current tenant prefix');
const deniedMimeUpload = await request('/api/tenant-content/assets/sign-upload', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
body: {
fileName: 'script.html',
assetType: 'document',
storageProvider: 'local_dev',
mimeType: 'text/html',
},
expectStatus: 400,
});
assert.equal(deniedMimeUpload.code, 'MIME_TYPE_NOT_ALLOWED', 'upload signer should reject unsupported mime types');
const deniedLargeUpload = await request('/api/tenant-content/assets/sign-upload', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
body: {
fileName: 'huge-video.mp4',
assetType: 'video',
storageProvider: 'local_dev',
mimeType: 'video/mp4',
fileSizeBytes: 1024 * 1024 * 1024,
},
expectStatus: 400,
});
assert.equal(deniedLargeUpload.code, 'FILE_TOO_LARGE', 'upload signer should enforce max upload size');
const asset = await request('/api/tenant-content/assets', {
userId: TENANT_ADMIN_USER_ID,
@@ -679,6 +755,50 @@ async function testTenantContentAssetsAndImports() {
assert.equal(download.item?.id, asset.item.id, 'svip student should receive asset download');
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');
const localAsset = await request('/api/tenant-content/assets', {
userId: TENANT_ADMIN_USER_ID,
method: 'PUT',
body: {
title: '集成测试本地对象资料',
assetType: 'pdf',
storageProvider: 'local_dev',
bucket: upload.assetDraft.bucket,
objectKey: upload.assetDraft.objectKey,
fileName: upload.assetDraft.fileName,
mimeType: upload.assetDraft.mimeType,
fileSizeBytes: upload.assetDraft.fileSizeBytes,
visibility: 'tenant',
status: 'active',
},
});
assert.equal(localAsset.item?.objectKey, upload.assetDraft.objectKey, 'asset upsert should keep normalized object key');
const localDownload = await request('/api/catalog/assets/download', {
query: { assetId: localAsset.item.id },
});
assert.equal(localDownload.download?.method, 'GET', 'object asset download should sign GET');
assert.equal(localDownload.download?.signatureMode, 'local-placeholder', 'local object asset should use local placeholder signer');
assert.ok(localDownload.download?.url?.includes(encodeURIComponent(localAsset.item.bucket)), 'object asset download should include bucket');
const invalidObjectAsset = await request('/api/tenant-content/assets', {
userId: TENANT_ADMIN_USER_ID,
method: 'PUT',
body: {
title: '非法对象键',
assetType: 'pdf',
storageProvider: 'local_dev',
bucket: 'tenant-assets',
objectKey: `${PARTNER_TENANT_ID}/pdf/unsafe.pdf`,
fileName: 'unsafe.pdf',
mimeType: 'application/pdf',
visibility: 'tenant',
status: 'active',
},
expectStatus: 400,
});
assert.equal(invalidObjectAsset.code, 'OBJECT_KEY_TENANT_PREFIX_REQUIRED', 'asset upsert should reject cross-tenant object keys');
const partnerAssetList = await request('/api/tenant-content/assets', {
tenantId: PARTNER_TENANT_ID,