fix(bucket-tenants): make tenant lifecycle and bucket import validation safer
This commit is contained in:
@@ -75,6 +75,17 @@ tap.test('should expose disabled cluster health in standalone mode', async () =>
|
||||
expect(clusterHealth.drives).toEqual(undefined);
|
||||
});
|
||||
|
||||
tap.test('should expose health and metrics with auth disabled', async () => {
|
||||
const health = await testSmartStorageInstance.getHealth();
|
||||
expect(health.running).toEqual(true);
|
||||
expect(health.auth.enabled).toEqual(false);
|
||||
expect(health.auth.tenantCredentialCount).toEqual(0);
|
||||
|
||||
const metrics = await testSmartStorageInstance.getMetrics();
|
||||
expect(metrics.tenantCredentialCount).toEqual(0);
|
||||
expect(metrics.prometheusText).toMatch(/smartstorage_tenant_credentials_total 0/);
|
||||
});
|
||||
|
||||
tap.test('should create a bucket', async () => {
|
||||
const response = await s3Client.send(new CreateBucketCommand({ Bucket: 'test-bucket' }));
|
||||
expect(response.$metadata.httpStatusCode).toEqual(200);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user