From fd890db7c8da51be42374d340228d645723f6112 Mon Sep 17 00:00:00 2001 From: A Ibrahim Date: Wed, 11 Mar 2026 10:52:03 +0100 Subject: [PATCH] fix(storage): disable acl list objects for new buckets --- packages/storage/src/lib/bucket/create.ts | 82 +++++---- .../test/bucket-create.integration.test.ts | 173 ++++++++++++++++++ 2 files changed, 216 insertions(+), 39 deletions(-) create mode 100644 packages/storage/src/test/bucket-create.integration.test.ts diff --git a/packages/storage/src/lib/bucket/create.ts b/packages/storage/src/lib/bucket/create.ts index 672fe2c..216ae91 100644 --- a/packages/storage/src/lib/bucket/create.ts +++ b/packages/storage/src/lib/bucket/create.ts @@ -73,6 +73,18 @@ export async function createBucket( } } + if ( + options?.sourceBucketSnapshot && + options.sourceBucketSnapshot !== '' && + (!options?.sourceBucketName || options.sourceBucketName === '') + ) { + return { + error: new Error( + 'sourceBucketName is required when sourceBucketSnapshot is provided' + ), + }; + } + const command = new CreateBucketCommand({ Bucket: bucketName, }); @@ -85,20 +97,27 @@ export async function createBucket( (next) => async (args) => { const req = args.request as HttpRequest; + // Disable directory listing by default + req.headers[TigrisHeaders.ACL_LIST_OBJECTS] = 'false'; + + // Set storage class if (options?.defaultTier) { req.headers[TigrisHeaders.STORAGE_CLASS] = options.defaultTier; } + // Set consistency level if (options?.consistency === 'strict') { req.headers[TigrisHeaders.CONSISTENT] = 'true'; } + // Set regions if (options?.region && options?.region !== undefined) { req.headers[TigrisHeaders.REGIONS] = Array.isArray(options.region) ? options.region.join(',') : options.region; } + // Set locations if ( options?.locations && options?.locations !== undefined && @@ -111,47 +130,32 @@ export async function createBucket( : options.locations.values; } - const result = await next(args); - return result; - }, - { - step: 'build', - } - ); + // Set snapshot enabled + if (options?.enableSnapshot) { + req.headers[TigrisHeaders.SNAPSHOT_ENABLED] = 'true'; + } - if (options?.enableSnapshot) { - command.middlewareStack.add( - (next) => async (args) => { - (args.request as HttpRequest).headers[TigrisHeaders.SNAPSHOT_ENABLED] = - 'true'; - return next(args); - }, - { step: 'build' } - ); - } + // Set fork source bucket + if (options?.sourceBucketName && options.sourceBucketName !== '') { + req.headers[TigrisHeaders.FORK_SOURCE_BUCKET] = + options.sourceBucketName; + } - if (options?.sourceBucketName && options.sourceBucketName !== '') { - const sourceBucketName = options.sourceBucketName; - command.middlewareStack.add( - (next) => async (args) => { - (args.request as HttpRequest).headers[ - TigrisHeaders.FORK_SOURCE_BUCKET - ] = sourceBucketName; - - if ( - options?.sourceBucketSnapshot && - options.sourceBucketSnapshot !== '' - ) { - (args.request as HttpRequest).headers[ - TigrisHeaders.FORK_SOURCE_BUCKET_SNAPSHOT - ] = options.sourceBucketSnapshot; - } - - return next(args); - }, - { step: 'build' } - ); - } + // Set fork source bucket snapshot + if ( + options?.sourceBucketName && + options.sourceBucketName !== '' && + options?.sourceBucketSnapshot && + options.sourceBucketSnapshot !== '' + ) { + req.headers[TigrisHeaders.FORK_SOURCE_BUCKET_SNAPSHOT] = + options.sourceBucketSnapshot; + } + + return await next(args); + }, + { step: 'build' } + ); try { return tigrisClient diff --git a/packages/storage/src/test/bucket-create.integration.test.ts b/packages/storage/src/test/bucket-create.integration.test.ts new file mode 100644 index 0000000..d5067ae --- /dev/null +++ b/packages/storage/src/test/bucket-create.integration.test.ts @@ -0,0 +1,173 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { createBucket } from '../lib/bucket/create'; +import { listBuckets } from '../lib/bucket/list'; +import { removeBucket } from '../lib/bucket/remove'; +import { shouldSkipIntegrationTests } from './setup'; +import { config } from '../lib/config'; + +const skipTests = shouldSkipIntegrationTests(); + +const testBucket = (suffix: string) => + `test-create-${suffix}-${Date.now()}`.toLowerCase(); + +describe.skipIf(skipTests)('createBucket Integration Tests', () => { + const bucketsToCleanup: string[] = []; + + afterEach(async () => { + for (const bucket of bucketsToCleanup) { + await removeBucket(bucket, { force: true, config }); + } + bucketsToCleanup.length = 0; + }); + + it('should create a bucket and be listable', async () => { + const name = testBucket('basic'); + bucketsToCleanup.push(name); + + const result = await createBucket(name, { config }); + + expect(result.error).toBeUndefined(); + expect(result.data).toBeDefined(); + expect(result.data?.hasForks).toBe(false); + expect(result.data?.isSnapshotEnabled).toBe(false); + + const { data } = await listBuckets({ config }); + expect(data?.buckets.some((b) => b.name === name)).toBe(true); + }); + + it('should create a public bucket with directory listing disabled', async () => { + const name = testBucket('public'); + bucketsToCleanup.push(name); + + const result = await createBucket(name, { + access: 'public', + config, + }); + + expect(result.error).toBeUndefined(); + expect(result.data).toBeDefined(); + + // Verify directory listing is disabled: unauthenticated ListObjects + // should return 403 even though the bucket is public + const endpoint = config.endpoint || 'https://t3.storage.dev'; + const resp = await fetch(`${endpoint}/${name}?list-type=2`); + expect(resp.status).toBe(403); + }); + + it('should create a bucket with snapshot enabled', async () => { + const name = testBucket('snap'); + bucketsToCleanup.push(name); + + const result = await createBucket(name, { + enableSnapshot: true, + config, + }); + + expect(result.error).toBeUndefined(); + expect(result.data?.isSnapshotEnabled).toBe(true); + }); + + it('should create a fork from a source bucket', async () => { + const sourceName = testBucket('fork-src'); + const forkName = testBucket('fork-dst'); + bucketsToCleanup.push(sourceName, forkName); + + await createBucket(sourceName, { + enableSnapshot: true, + config, + }); + + const result = await createBucket(forkName, { + sourceBucketName: sourceName, + config, + }); + + expect(result.error).toBeUndefined(); + expect(result.data?.sourceBucketName).toBe(sourceName); + }); + + it('should accept all location types', async () => { + const locations = [ + { + suffix: 'single', + opts: { type: 'single' as const, values: 'iad' as const }, + }, + { + suffix: 'multi', + opts: { type: 'multi' as const, values: 'usa' as const }, + }, + { + suffix: 'dual', + opts: { + type: 'dual' as const, + values: ['iad', 'fra'] as ['iad', 'fra'], + }, + }, + { suffix: 'global', opts: { type: 'global' as const } }, + ]; + + for (const { suffix, opts } of locations) { + const name = testBucket(`loc-${suffix}`); + bucketsToCleanup.push(name); + + const result = await createBucket(name, { + locations: opts, + config, + }); + + expect(result.error, `${suffix} location failed`).toBeUndefined(); + expect(result.data).toBeDefined(); + } + }); + + it('should create a bucket with all options combined', async () => { + const name = testBucket('combined'); + bucketsToCleanup.push(name); + + const result = await createBucket(name, { + access: 'public', + defaultTier: 'STANDARD_IA', + enableSnapshot: true, + locations: { type: 'single', values: 'iad' }, + config, + }); + + expect(result.error).toBeUndefined(); + expect(result.data).toBeDefined(); + expect(result.data?.isSnapshotEnabled).toBe(true); + }); + + // ── Validation errors ── + + it('should return error for invalid region', async () => { + const result = await createBucket(testBucket('bad-region'), { + region: 'invalid-region', + config, + }); + + expect(result.error).toBeDefined(); + expect(result.error?.message).toContain('Invalid regions specified'); + }); + + it('should return error for invalid location', async () => { + const result = await createBucket(testBucket('bad-loc'), { + locations: { type: 'single', values: 'invalid' as 'iad' }, + config, + }); + + expect(result.error).toBeDefined(); + expect(result.error?.message).toContain('Invalid single-region location'); + }); + + it('should return error when sourceBucketSnapshot is provided without sourceBucketName', async () => { + const result = await createBucket(testBucket('snap-no-src'), { + sourceBucketSnapshot: 'snap-123', + config, + }); + + expect(result.error).toBeDefined(); + expect(result.error?.message).toBe( + 'sourceBucketName is required when sourceBucketSnapshot is provided' + ); + }); +});