259 lines
11 KiB
TypeScript
259 lines
11 KiB
TypeScript
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<IReq_CreateBucket>(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<IReq_DeleteBucket>(url, 'deleteBucket');
|
|
await del.fire({ identity, bucketName: 'pol-bucket-1' });
|
|
} catch { /* may already be deleted */ }
|
|
try {
|
|
const del = new TypedRequest<IReq_DeleteBucket>(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<IReq_ListNamedPolicies>(url, 'listNamedPolicies');
|
|
const response = await req.fire({ identity });
|
|
assertEquals(response.policies.length, 0);
|
|
});
|
|
|
|
it('should create a named policy', async () => {
|
|
const req = new TypedRequest<IReq_CreateNamedPolicy>(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<IReq_CreateNamedPolicy>(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<IReq_ListNamedPolicies>(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<IReq_GetBucketNamedPolicies>(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<IReq_AttachPolicyToBucket>(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<IReq_GetBucketPolicy>(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<IReq_GetBucketNamedPolicies>(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<IReq_AttachPolicyToBucket>(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<IReq_GetBucketPolicy>(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<IReq_GetPolicyBuckets>(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<IReq_SetPolicyBuckets>(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<IReq_GetBucketPolicy>(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<IReq_UpdateNamedPolicy>(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<IReq_GetBucketPolicy>(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<IReq_DetachPolicyFromBucket>(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<IReq_GetBucketPolicy>(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<IReq_DeleteNamedPolicy>(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<IReq_DeleteBucket>(url, 'deleteBucket');
|
|
await delBucket.fire({ identity, bucketName: 'pol-bucket-1' });
|
|
|
|
// Verify policy1 no longer lists pol-bucket-1
|
|
const req = new TypedRequest<IReq_GetPolicyBuckets>(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<IReq_DeleteNamedPolicy>(url, 'deleteNamedPolicy');
|
|
await delPolicy.fire({ identity, policyId: policy1Id });
|
|
|
|
const listPolicies = new TypedRequest<IReq_ListNamedPolicies>(url, 'listNamedPolicies');
|
|
const response = await listPolicies.fire({ identity });
|
|
assertEquals(response.policies.length, 0);
|
|
});
|
|
});
|