|
| 1 | +import "@aws-sdk/signature-v4a"; |
| 2 | + |
| 3 | +import { |
| 4 | + CreateBucketCommand, |
| 5 | + HeadBucketCommand, |
| 6 | + ListObjectsV2Command, |
| 7 | + S3Client, |
| 8 | + waitUntilBucketExists, |
| 9 | +} from "@aws-sdk/client-s3"; |
| 10 | +import { |
| 11 | + CreateMultiRegionAccessPointCommand, |
| 12 | + DescribeMultiRegionAccessPointOperationCommand, |
| 13 | + GetMultiRegionAccessPointCommand, |
| 14 | + Region as S3ControlRegion, |
| 15 | + S3ControlClient, |
| 16 | +} from "@aws-sdk/client-s3-control"; |
| 17 | +import { GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts"; |
| 18 | +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; |
| 19 | + |
| 20 | +const MRAP_NAME = "jsv3-e2e-mrap-sigv4a-min"; |
| 21 | +const BUCKET_PREFIX = "jsv3-e2e-mrap-sigv4a-min-"; |
| 22 | +const REGION_1 = "us-west-2"; |
| 23 | +const REGION_2 = "us-east-1"; |
| 24 | +const POLLING_DELAY_SECONDS = 10; |
| 25 | +const OPERATION_TIMEOUT_MINUTES = 15; |
| 26 | + |
| 27 | +describe("S3 MRAP SigV4a E2E Test", () => { |
| 28 | + let s3ControlClient: S3ControlClient; |
| 29 | + let accountId: string; |
| 30 | + let mrapArn: string | null = null; |
| 31 | + let bucket1Name: string; |
| 32 | + let bucket2Name: string; |
| 33 | + |
| 34 | + vi.setConfig({ hookTimeout: OPERATION_TIMEOUT_MINUTES * 60 * 1000 }); |
| 35 | + |
| 36 | + const pollOperation = async (requestTokenArn: string): Promise<void> => { |
| 37 | + const deadline = Date.now() + OPERATION_TIMEOUT_MINUTES * 60 * 1000; |
| 38 | + while (Date.now() < deadline) { |
| 39 | + const { AsyncOperation } = await s3ControlClient.send( |
| 40 | + new DescribeMultiRegionAccessPointOperationCommand({ AccountId: accountId, RequestTokenARN: requestTokenArn }) |
| 41 | + ); |
| 42 | + if (AsyncOperation?.RequestStatus === "SUCCEEDED") return; |
| 43 | + if (AsyncOperation?.RequestStatus === "FAILED") throw new Error(`S3Control operation failed`); |
| 44 | + await new Promise((resolve) => setTimeout(resolve, POLLING_DELAY_SECONDS * 1000)); |
| 45 | + } |
| 46 | + throw new Error("S3Control operation timed out."); |
| 47 | + }; |
| 48 | + |
| 49 | + const ensureBucket = async (bucketName: string, region: string): Promise<void> => { |
| 50 | + const s3 = new S3Client({ region }); |
| 51 | + try { |
| 52 | + await s3.send(new HeadBucketCommand({ Bucket: bucketName })); |
| 53 | + } catch (error: any) { |
| 54 | + if (error.name === "NotFound") { |
| 55 | + await s3.send(new CreateBucketCommand({ Bucket: bucketName })); |
| 56 | + await waitUntilBucketExists({ client: s3, maxWaitTime: 120 }, { Bucket: bucketName }); |
| 57 | + } else { |
| 58 | + throw error; |
| 59 | + } |
| 60 | + } finally { |
| 61 | + s3.destroy(); |
| 62 | + } |
| 63 | + }; |
| 64 | + |
| 65 | + beforeAll(async () => { |
| 66 | + const stsClient = new STSClient({ region: REGION_1 }); |
| 67 | + s3ControlClient = new S3ControlClient({ region: REGION_1 }); |
| 68 | + try { |
| 69 | + const { Account } = await stsClient.send(new GetCallerIdentityCommand({})); |
| 70 | + if (!Account) throw new Error("Could not determine AWS Account ID."); |
| 71 | + accountId = Account; |
| 72 | + } finally { |
| 73 | + stsClient.destroy(); |
| 74 | + } |
| 75 | + |
| 76 | + bucket1Name = `${BUCKET_PREFIX}${accountId}-${REGION_1}`; |
| 77 | + bucket2Name = `${BUCKET_PREFIX}${accountId}-${REGION_2}`; |
| 78 | + |
| 79 | + await ensureBucket(bucket1Name, REGION_1); |
| 80 | + await ensureBucket(bucket2Name, REGION_2); |
| 81 | + |
| 82 | + try { |
| 83 | + const { AccessPoint } = await s3ControlClient.send( |
| 84 | + new GetMultiRegionAccessPointCommand({ AccountId: accountId, Name: MRAP_NAME }) |
| 85 | + ); |
| 86 | + mrapArn = `arn:aws:s3::${accountId}:accesspoint/${AccessPoint?.Alias}`; |
| 87 | + } catch (error) { |
| 88 | + const createResponse = await s3ControlClient.send( |
| 89 | + new CreateMultiRegionAccessPointCommand({ |
| 90 | + AccountId: accountId, |
| 91 | + Details: { |
| 92 | + Name: MRAP_NAME, |
| 93 | + Regions: [{ Bucket: bucket1Name }, { Bucket: bucket2Name }] as S3ControlRegion[], |
| 94 | + PublicAccessBlock: { |
| 95 | + BlockPublicAcls: true, |
| 96 | + IgnorePublicAcls: true, |
| 97 | + BlockPublicPolicy: true, |
| 98 | + RestrictPublicBuckets: true, |
| 99 | + }, |
| 100 | + }, |
| 101 | + }) |
| 102 | + ); |
| 103 | + if (!createResponse.RequestTokenARN) throw new Error("Create MRAP did not return token."); |
| 104 | + await pollOperation(createResponse.RequestTokenARN); |
| 105 | + const { AccessPoint } = await s3ControlClient.send( |
| 106 | + new GetMultiRegionAccessPointCommand({ AccountId: accountId, Name: MRAP_NAME }) |
| 107 | + ); |
| 108 | + mrapArn = `arn:aws:s3::${accountId}:accesspoint/${AccessPoint?.Alias}`; |
| 109 | + } |
| 110 | + if (!mrapArn) throw new Error("Failed to get MRAP ARN."); |
| 111 | + }); |
| 112 | + |
| 113 | + afterAll(async () => { |
| 114 | + if (s3ControlClient) s3ControlClient.destroy(); |
| 115 | + }); |
| 116 | + |
| 117 | + it("should successfully ListObjectsV2 against MRAP using SigV4a", async () => { |
| 118 | + expect(mrapArn).toBeDefined(); |
| 119 | + |
| 120 | + const s3SigV4aClient = new S3Client({ region: REGION_1 }); |
| 121 | + |
| 122 | + try { |
| 123 | + const command = new ListObjectsV2Command({ Bucket: mrapArn! }); |
| 124 | + const response = await s3SigV4aClient.send(command); |
| 125 | + |
| 126 | + expect(response.$metadata.httpStatusCode).toBe(200); |
| 127 | + expect(response.Contents ?? []).toBeInstanceOf(Array); |
| 128 | + } catch (error) { |
| 129 | + console.error("ListObjectsV2 against MRAP failed:", error); |
| 130 | + throw error; |
| 131 | + } finally { |
| 132 | + s3SigV4aClient.destroy(); |
| 133 | + } |
| 134 | + }); |
| 135 | +}); |
0 commit comments