fix(bucket-tenants): make tenant lifecycle and bucket import validation safer

This commit is contained in:
2026-05-02 12:09:13 +00:00
parent 7020810b5e
commit b075de1ecd
23 changed files with 435 additions and 183 deletions
+75 -6
View File
@@ -23,7 +23,10 @@ const STORAGE_DIR = fileURLToPath(new URL('../.nogit/bucket-tenant-tests', impor
const WORKAPP_A_BUCKET = 'workapp-a-bucket';
const WORKAPP_B_BUCKET = 'workapp-b-bucket';
const RESTORE_BUCKET = 'workapp-a-restore-bucket';
const CORRUPT_RESTORE_BUCKET = 'workapp-a-corrupt-restore-bucket';
const POLICY_BUCKET = 'workapp-policy-bucket';
const DUPLICATE_TENANT_BUCKET = 'workapp-duplicate-tenant-bucket';
const REVOKE_ONLY_BUCKET = 'workapp-revoke-only-bucket';
const ADMIN_CREDENTIAL: smartstorage.IStorageCredential = {
accessKeyId: 'TENANTADMIN',
secretAccessKey: 'TENANTADMINSECRET123',
@@ -94,6 +97,20 @@ async function startStorage() {
adminClient = createS3Client(ADMIN_CREDENTIAL);
}
tap.test('bucket tenant client APIs require auth before IPC', async () => {
const storage = new smartstorage.SmartStorage({
auth: {
enabled: false,
credentials: [ADMIN_CREDENTIAL],
},
});
await expect(storage.listBucketTenants()).rejects.toThrow();
await expect(storage.getBucketTenantDescriptor({
bucketName: WORKAPP_A_BUCKET,
})).rejects.toThrow();
});
tap.test('setup: start storage and provision bucket tenants', async () => {
await rm(STORAGE_DIR, { recursive: true, force: true });
await startStorage();
@@ -129,6 +146,18 @@ tap.test('listBucketTenants returns scoped credential metadata without secrets',
expect((tenants[0] as any).secretAccessKey).toEqual(undefined);
});
tap.test('createBucketTenant validates credentials before creating buckets', async () => {
await expect(testSmartStorageInstance.createBucketTenant({
bucketName: DUPLICATE_TENANT_BUCKET,
accessKeyId: ADMIN_CREDENTIAL.accessKeyId,
secretAccessKey: 'DUPLICATESECRET123',
})).rejects.toThrow();
await expect(adminClient.send(new HeadBucketCommand({
Bucket: DUPLICATE_TENANT_BUCKET,
}))).rejects.toThrow();
});
tap.test('tenant credentials work with AWS SDK v3 for their assigned bucket', async () => {
const putA = await tenantAClient.send(new PutObjectCommand({
Bucket: WORKAPP_A_BUCKET,
@@ -227,6 +256,21 @@ tap.test('export/import targets one bucket without unrelated tenant data', async
Bucket: RESTORE_BUCKET,
}));
expect(restoredObjects.Contents?.some((object) => object.Key === 'other.txt')).toEqual(false);
const corruptExport: smartstorage.IBucketExport = {
...bucketExport,
objects: bucketExport.objects.map((object, index) => index === 0 ? {
...object,
size: object.size + 1,
} : object),
};
await expect(testSmartStorageInstance.importBucket({
bucketName: CORRUPT_RESTORE_BUCKET,
source: corruptExport,
})).rejects.toThrow();
await expect(adminClient.send(new HeadBucketCommand({
Bucket: CORRUPT_RESTORE_BUCKET,
}))).rejects.toThrow();
});
tap.test('bucket policies persist across restart', async () => {
@@ -301,25 +345,50 @@ tap.test('runtime credentials survive restart', async () => {
expect(policyResponse.Policy?.includes('TenantPolicyPersistence')).toEqual(true);
});
tap.test('deleteBucketTenant refuses buckets without tenant credentials', async () => {
await expect(testSmartStorageInstance.deleteBucketTenant({
bucketName: POLICY_BUCKET,
})).rejects.toThrow();
const headAfterRefusedDelete = await adminClient.send(new HeadBucketCommand({
Bucket: POLICY_BUCKET,
}));
expect(headAfterRefusedDelete.$metadata.httpStatusCode).toEqual(200);
});
tap.test('deleteBucketTenant can revoke credentials and delete tenant buckets', async () => {
const revokeOnlyTenant = await testSmartStorageInstance.createBucketTenant({
bucketName: REVOKE_ONLY_BUCKET,
});
const revokeOnlyClient = createS3ClientFromDescriptor(revokeOnlyTenant);
await revokeOnlyClient.send(new PutObjectCommand({
Bucket: REVOKE_ONLY_BUCKET,
Key: 'revoke-only.txt',
Body: 'revocation target',
}));
await testSmartStorageInstance.deleteBucketTenant({
bucketName: WORKAPP_B_BUCKET,
accessKeyId: tenantB.accessKeyId,
bucketName: REVOKE_ONLY_BUCKET,
accessKeyId: revokeOnlyTenant.accessKeyId,
});
await expect(tenantBClient.send(new GetObjectCommand({
Bucket: WORKAPP_B_BUCKET,
Key: 'other.txt',
await expect(revokeOnlyClient.send(new GetObjectCommand({
Bucket: REVOKE_ONLY_BUCKET,
Key: 'revoke-only.txt',
}))).rejects.toThrow();
const headAfterRevoke = await adminClient.send(new HeadBucketCommand({
Bucket: WORKAPP_B_BUCKET,
Bucket: REVOKE_ONLY_BUCKET,
}));
expect(headAfterRevoke.$metadata.httpStatusCode).toEqual(200);
await testSmartStorageInstance.deleteBucketTenant({
bucketName: WORKAPP_B_BUCKET,
});
await expect(tenantBClient.send(new GetObjectCommand({
Bucket: WORKAPP_B_BUCKET,
Key: 'other.txt',
}))).rejects.toThrow();
await expect(adminClient.send(new HeadBucketCommand({
Bucket: WORKAPP_B_BUCKET,
}))).rejects.toThrow();