import { assertEquals, assertExists } from 'jsr:@std/assert'; import { afterAll, beforeAll, describe, it } from 'jsr:@std/testing/bdd'; import { TypedRequest } from '@api.global/typedrequest'; import { createTestContainer, getTestPorts, loginAndGetIdentity } from './helpers/server.helper.ts'; import { ObjectStorageContainer } from '../ts/index.ts'; import type * as interfaces from '../ts_interfaces/index.ts'; import type { IReq_CreateBucket, IReq_DeleteBucket, IReq_GetBucketPolicy } from '../ts_interfaces/requests/buckets.ts'; import type { IReq_ListNamedPolicies, IReq_CreateNamedPolicy, IReq_UpdateNamedPolicy, IReq_DeleteNamedPolicy, IReq_GetBucketNamedPolicies, IReq_AttachPolicyToBucket, IReq_DetachPolicyFromBucket, IReq_GetPolicyBuckets, IReq_SetPolicyBuckets, } from '../ts_interfaces/requests/policies.ts'; const PORT_INDEX = 5; const ports = getTestPorts(PORT_INDEX); const url = `http://localhost:${ports.uiPort}/typedrequest`; describe('Named policy management', { sanitizeResources: false, sanitizeOps: false }, () => { let container: ObjectStorageContainer; let identity: interfaces.data.IIdentity; let policy1Id: string; let policy2Id: string; beforeAll(async () => { container = createTestContainer(PORT_INDEX); await container.start(); identity = await loginAndGetIdentity(ports.uiPort); // Create test buckets const createBucket = new TypedRequest(url, 'createBucket'); await createBucket.fire({ identity, bucketName: 'pol-bucket-1' }); await createBucket.fire({ identity, bucketName: 'pol-bucket-2' }); }); afterAll(async () => { // Cleanup try { const del = new TypedRequest(url, 'deleteBucket'); await del.fire({ identity, bucketName: 'pol-bucket-1' }); } catch { /* may already be deleted */ } try { const del = new TypedRequest(url, 'deleteBucket'); await del.fire({ identity, bucketName: 'pol-bucket-2' }); } catch { /* may already be deleted */ } await container.stop(); }); it('should list policies (initially empty)', async () => { const req = new TypedRequest(url, 'listNamedPolicies'); const response = await req.fire({ identity }); assertEquals(response.policies.length, 0); }); it('should create a named policy', async () => { const req = new TypedRequest(url, 'createNamedPolicy'); const response = await req.fire({ identity, name: 'Public Read', description: 'Allows public read access', statements: [ { Sid: 'PublicRead', Effect: 'Allow', Principal: '*', Action: 's3:GetObject', Resource: 'arn:aws:s3:::${bucket}/*', }, ], }); assertExists(response.policy.id); assertEquals(response.policy.name, 'Public Read'); assertEquals(response.policy.statements.length, 1); assertEquals(response.policy.createdAt > 0, true); policy1Id = response.policy.id; }); it('should create a second policy', async () => { const req = new TypedRequest(url, 'createNamedPolicy'); const response = await req.fire({ identity, name: 'Deny Delete', description: 'Denies delete actions', statements: [ { Sid: 'DenyDelete', Effect: 'Deny', Principal: '*', Action: 's3:DeleteObject', Resource: 'arn:aws:s3:::${bucket}/*', }, ], }); policy2Id = response.policy.id; assertExists(policy2Id); }); it('should list both policies', async () => { const req = new TypedRequest(url, 'listNamedPolicies'); const response = await req.fire({ identity }); assertEquals(response.policies.length, 2); }); it('should get bucket named policies (none attached)', async () => { const req = new TypedRequest(url, 'getBucketNamedPolicies'); const response = await req.fire({ identity, bucketName: 'pol-bucket-1' }); assertEquals(response.attachedPolicies.length, 0); assertEquals(response.availablePolicies.length, 2); }); it('should attach policy to bucket', async () => { const req = new TypedRequest(url, 'attachPolicyToBucket'); const response = await req.fire({ identity, policyId: policy1Id, bucketName: 'pol-bucket-1' }); assertEquals(response.ok, true); }); it('should verify bucket policy was applied with placeholder replaced', async () => { const req = new TypedRequest(url, 'getBucketPolicy'); const response = await req.fire({ identity, bucketName: 'pol-bucket-1' }); assertExists(response.policy); const parsed = JSON.parse(response.policy!); const resource = parsed.Statement[0].Resource; // Resource may be a string or array depending on the S3 engine const resourceStr = Array.isArray(resource) ? resource.join(' ') : resource; // ${bucket} should be replaced with actual bucket name assertEquals(resourceStr.includes('pol-bucket-1'), true); assertEquals(resourceStr.includes('${bucket}'), false); }); it('should get bucket named policies (one attached)', async () => { const req = new TypedRequest(url, 'getBucketNamedPolicies'); const response = await req.fire({ identity, bucketName: 'pol-bucket-1' }); assertEquals(response.attachedPolicies.length, 1); assertEquals(response.availablePolicies.length, 1); assertEquals(response.attachedPolicies[0].id, policy1Id); }); it('should attach second policy to same bucket', async () => { const req = new TypedRequest(url, 'attachPolicyToBucket'); const response = await req.fire({ identity, policyId: policy2Id, bucketName: 'pol-bucket-1' }); assertEquals(response.ok, true); }); it('should verify merged policy has statements from both', async () => { const req = new TypedRequest(url, 'getBucketPolicy'); const response = await req.fire({ identity, bucketName: 'pol-bucket-1' }); assertExists(response.policy); const parsed = JSON.parse(response.policy!); assertEquals(parsed.Statement.length >= 2, true); const sids = parsed.Statement.map((s: any) => s.Sid); assertEquals(sids.includes('PublicRead'), true); assertEquals(sids.includes('DenyDelete'), true); }); it('should get policy buckets', async () => { const req = new TypedRequest(url, 'getPolicyBuckets'); const response = await req.fire({ identity, policyId: policy1Id }); assertEquals(response.attachedBuckets.includes('pol-bucket-1'), true); assertEquals(response.availableBuckets.includes('pol-bucket-2'), true); }); it('should set policy buckets (batch)', async () => { const req = new TypedRequest(url, 'setPolicyBuckets'); const response = await req.fire({ identity, policyId: policy1Id, bucketNames: ['pol-bucket-1', 'pol-bucket-2'], }); assertEquals(response.ok, true); }); it('should verify policy applied to second bucket', async () => { const req = new TypedRequest(url, 'getBucketPolicy'); const response = await req.fire({ identity, bucketName: 'pol-bucket-2' }); assertExists(response.policy); const parsed = JSON.parse(response.policy!); const resource = parsed.Statement[0].Resource; const resourceStr = Array.isArray(resource) ? resource.join(' ') : resource; assertEquals(resourceStr.includes('pol-bucket-2'), true); }); it('should update a policy', async () => { const req = new TypedRequest(url, 'updateNamedPolicy'); const response = await req.fire({ identity, policyId: policy1Id, name: 'Public Read Updated', description: 'Updated policy', statements: [ { Sid: 'PublicReadUpdated', Effect: 'Allow', Principal: '*', Action: ['s3:GetObject', 's3:ListBucket'], Resource: 'arn:aws:s3:::${bucket}/*', }, ], }); assertEquals(response.policy.name, 'Public Read Updated'); assertEquals(response.policy.updatedAt >= response.policy.createdAt, true); }); it('should verify updated policy cascaded to attached buckets', async () => { const req = new TypedRequest(url, 'getBucketPolicy'); const response = await req.fire({ identity, bucketName: 'pol-bucket-1' }); assertExists(response.policy); const parsed = JSON.parse(response.policy!); const sids = parsed.Statement.map((s: any) => s.Sid); assertEquals(sids.includes('PublicReadUpdated'), true); }); it('should detach policy from bucket', async () => { const req = new TypedRequest(url, 'detachPolicyFromBucket'); const response = await req.fire({ identity, policyId: policy2Id, bucketName: 'pol-bucket-1' }); assertEquals(response.ok, true); }); it('should verify bucket policy updated after detach', async () => { const req = new TypedRequest(url, 'getBucketPolicy'); const response = await req.fire({ identity, bucketName: 'pol-bucket-1' }); assertExists(response.policy); const parsed = JSON.parse(response.policy!); const sids = parsed.Statement.map((s: any) => s.Sid); assertEquals(sids.includes('DenyDelete'), false); assertEquals(sids.includes('PublicReadUpdated'), true); }); it('should delete a named policy', async () => { const req = new TypedRequest(url, 'deleteNamedPolicy'); const response = await req.fire({ identity, policyId: policy2Id }); assertEquals(response.ok, true); }); it('should handle bucket deletion cleaning up attachments', async () => { // Delete pol-bucket-1 which has policy1 attached const delBucket = new TypedRequest(url, 'deleteBucket'); await delBucket.fire({ identity, bucketName: 'pol-bucket-1' }); // Verify policy1 no longer lists pol-bucket-1 const req = new TypedRequest(url, 'getPolicyBuckets'); const response = await req.fire({ identity, policyId: policy1Id }); assertEquals(response.attachedBuckets.includes('pol-bucket-1'), false); }); it('cleanup: delete remaining policy and bucket', async () => { const delPolicy = new TypedRequest(url, 'deleteNamedPolicy'); await delPolicy.fire({ identity, policyId: policy1Id }); const listPolicies = new TypedRequest(url, 'listNamedPolicies'); const response = await listPolicies.fire({ identity }); assertEquals(response.policies.length, 0); }); });