1+ import "@smithy/signature-v4a" ;
2+
3+ import { Sha256 } from "@aws-crypto/sha256-js" ;
4+ import {
5+ S3Client ,
6+ CreateBucketCommand ,
7+ DeleteBucketCommand ,
8+ PutObjectCommand ,
9+ ListObjectsV2Command
10+ } from "@aws-sdk/client-s3" ;
11+ import {
12+ S3ControlClient ,
13+ CreateMultiRegionAccessPointCommand ,
14+ DeleteMultiRegionAccessPointCommand ,
15+ DescribeMultiRegionAccessPointOperationCommand ,
16+ GetMultiRegionAccessPointCommand
17+ } from "@aws-sdk/client-s3-control" ;
18+ import { GetCallerIdentityCommand , STSClient } from "@aws-sdk/client-sts" ;
19+ import { SignatureV4MultiRegion } from "@aws-sdk/signature-v4-multi-region" ;
20+ import { HttpRequest } from "@smithy/protocol-http" ;
21+
22+ jest . setTimeout ( 1800000 ) ; // 30 minutes (MRAP operations can take a while)
23+
24+ describe ( "S3 Multi-Region Access Point with SignatureV4a (JS Implementation)" , ( ) => {
25+ let s3Client : S3Client ;
26+ let s3ControlClient : S3ControlClient ;
27+ let accountId : string ;
28+ let signer : SignatureV4MultiRegion ;
29+ let mrapName : string ;
30+ let bucketName1 : string ;
31+ let bucketName2 : string ;
32+ let mrapArn : string ;
33+
34+ beforeAll ( async ( ) => {
35+ const stsClient = new STSClient ( { } ) ;
36+ const { Account } = await stsClient . send ( new GetCallerIdentityCommand ( { } ) ) ;
37+ accountId = Account ! ;
38+ const timestamp = Date . now ( ) ;
39+ mrapName = `test-mrap-${ timestamp } ` ;
40+ bucketName1 = `test-bucket1-${ timestamp } ` ;
41+ bucketName2 = `test-bucket2-${ timestamp } ` ;
42+
43+ signer = new SignatureV4MultiRegion ( {
44+ service : "s3" ,
45+ region : "*" ,
46+ sha256 : Sha256 ,
47+ credentials : {
48+ accessKeyId : process . env . AWS_ACCESS_KEY_ID ! ,
49+ secretAccessKey : process . env . AWS_SECRET_ACCESS_KEY ! ,
50+ sessionToken : process . env . AWS_SESSION_TOKEN ,
51+ } ,
52+ } ) ;
53+
54+ s3Client = new S3Client ( {
55+ region : "*" ,
56+ useArnRegion : true ,
57+ signer,
58+ } ) ;
59+
60+ s3ControlClient = new S3ControlClient ( {
61+ region : "*" ,
62+ signer,
63+ } ) ;
64+
65+ // Create buckets
66+ await s3Client . send ( new CreateBucketCommand ( { Bucket : bucketName1 , CreateBucketConfiguration : { LocationConstraint : "us-west-2" } } ) ) ;
67+ await s3Client . send ( new CreateBucketCommand ( { Bucket : bucketName2 , CreateBucketConfiguration : { LocationConstraint : "us-east-2" } } ) ) ;
68+
69+ // Create MRAP
70+ const createResponse = await s3ControlClient . send (
71+ new CreateMultiRegionAccessPointCommand ( {
72+ AccountId : accountId ,
73+ ClientToken : `create-${ timestamp } ` ,
74+ Details : {
75+ Name : mrapName ,
76+ PublicAccessBlock : {
77+ BlockPublicAcls : true ,
78+ BlockPublicPolicy : true ,
79+ IgnorePublicAcls : true ,
80+ RestrictPublicBuckets : true ,
81+ } ,
82+ Regions : [
83+ { Bucket : bucketName1 , BucketAccountId : accountId } ,
84+ { Bucket : bucketName2 , BucketAccountId : accountId } ,
85+ ] ,
86+ } ,
87+ } )
88+ ) ;
89+
90+ // Wait for MRAP to be created
91+ let mrapReady = false ;
92+ let retries = 0 ;
93+ while ( ! mrapReady && retries < 60 ) {
94+ const describeResponse = await s3ControlClient . send (
95+ new DescribeMultiRegionAccessPointOperationCommand ( {
96+ AccountId : accountId ,
97+ RequestTokenARN : createResponse . RequestTokenARN ,
98+ } )
99+ ) ;
100+
101+ if ( describeResponse . AsyncOperation ?. RequestStatus === "SUCCESS" ) {
102+ mrapReady = true ;
103+ } else {
104+ await new Promise ( resolve => setTimeout ( resolve , 30000 ) ) ; // Wait for 30 seconds before retrying
105+ retries ++ ;
106+ }
107+ }
108+
109+ if ( ! mrapReady ) {
110+ throw new Error ( "MRAP creation timed out" ) ;
111+ }
112+
113+ // Get MRAP ARN
114+ const getResponse = await s3ControlClient . send (
115+ new GetMultiRegionAccessPointCommand ( {
116+ AccountId : accountId ,
117+ Name : mrapName ,
118+ } )
119+ ) ;
120+ mrapArn = getResponse . AccessPoint ! . Alias ! ;
121+
122+ // Upload a small file to one of the buckets
123+ await s3Client . send ( new PutObjectCommand ( {
124+ Bucket : bucketName1 ,
125+ Key : "testfile" ,
126+ Body : Buffer . from ( "test" , "utf-8" )
127+ } ) ) ;
128+ } ) ;
129+
130+ afterAll ( async ( ) => {
131+ // Delete MRAP
132+ try {
133+ await s3ControlClient . send (
134+ new DeleteMultiRegionAccessPointCommand ( {
135+ AccountId : accountId ,
136+ ClientToken : `delete-${ Date . now ( ) } ` ,
137+ Details : {
138+ Name : mrapName ,
139+ } ,
140+ } )
141+ ) ;
142+ } catch ( error ) {
143+ console . error ( "Failed to initiate deletion of Multi-Region Access Point:" , error ) ;
144+ }
145+
146+ // Delete buckets
147+ try {
148+ await s3Client . send ( new DeleteBucketCommand ( { Bucket : bucketName1 } ) ) ;
149+ await s3Client . send ( new DeleteBucketCommand ( { Bucket : bucketName2 } ) ) ;
150+ } catch ( error ) {
151+ console . error ( "Failed to delete buckets:" , error ) ;
152+ }
153+ } ) ;
154+
155+ it ( "should use SignatureV4a JS implementation" , async ( ) => {
156+ const mockRequest = new HttpRequest ( {
157+ method : "GET" ,
158+ protocol : "https:" ,
159+ hostname : "s3-global.amazonaws.com" ,
160+ headers : {
161+ host : "s3-global.amazonaws.com" ,
162+ } ,
163+ path : "/" ,
164+ } ) ;
165+
166+ const signSpy = jest . spyOn ( signer , "sign" ) ;
167+
168+ await signer . sign ( mockRequest , { signingRegion : "*" } ) ;
169+
170+ expect ( signSpy ) . toHaveBeenCalled ( ) ;
171+ const signArgs = signSpy . mock . calls [ 0 ] ;
172+ expect ( signArgs [ 1 ] ?. signingRegion ) . toBe ( "*" ) ;
173+
174+ // verify that signed request has the expected SigV4a headers
175+ const signedRequest = await signSpy . mock . results [ 0 ] . value ;
176+ expect ( signedRequest . headers [ "x-amz-region-set" ] ) . toBe ( "*" ) ;
177+ expect ( signedRequest . headers [ "authorization" ] ) . toContain ( "AWS4-ECDSA-P256-SHA256" ) ;
178+
179+ signSpy . mockRestore ( ) ;
180+ } ) ;
181+
182+ it ( "should list objects through MRAP using SignatureV4a" , async ( ) => {
183+ const command = new ListObjectsV2Command ( {
184+ Bucket : mrapArn ,
185+ } ) ;
186+
187+ const response = await s3Client . send ( command ) ;
188+
189+ expect ( response . Contents ) . toBeDefined ( ) ;
190+ expect ( response . Contents ?. length ) . toBeGreaterThan ( 0 ) ;
191+ expect ( response . Contents ?. some ( object => object . Key === "testfile" ) ) . toBe ( true ) ;
192+ } ) ;
193+ } ) ;
0 commit comments