@@ -12,14 +12,29 @@ jest.mock("./remoteProvider/retry");
12
12
jest . mock ( "./remoteProvider/RemoteProviderInit" ) ;
13
13
14
14
describe ( "fromInstanceMetadata" , ( ) => {
15
+ const host = "169.254.169.254" ;
15
16
const mockTimeout = 1000 ;
16
17
const mockMaxRetries = 3 ;
17
- const mockProfile = "foo" ;
18
+ const mockToken = "fooToken" ;
19
+ const mockProfile = "fooProfile" ;
18
20
19
- const mockHttpRequestOptions = {
20
- host : "169.254.169.254" ,
21
+ const mockTokenRequestOptions = {
22
+ host,
23
+ path : "/latest/api/token" ,
24
+ method : "PUT" ,
25
+ headers : {
26
+ "x-aws-ec2-metadata-token-ttl-seconds" : "21600" ,
27
+ } ,
28
+ timeout : mockTimeout ,
29
+ } ;
30
+
31
+ const mockProfileRequestOptions = {
32
+ host,
21
33
path : "/latest/meta-data/iam/security-credentials/" ,
22
34
timeout : mockTimeout ,
35
+ headers : {
36
+ "x-aws-ec2-metadata-token" : mockToken ,
37
+ } ,
23
38
} ;
24
39
25
40
const mockImdsCreds = Object . freeze ( {
@@ -48,35 +63,38 @@ describe("fromInstanceMetadata", () => {
48
63
jest . resetAllMocks ( ) ;
49
64
} ) ;
50
65
51
- it ( "gets profile name from IMDS, and passes profile name to fetch credentials" , async ( ) => {
52
- ( httpRequest as jest . Mock ) . mockResolvedValueOnce ( mockProfile ) . mockResolvedValueOnce ( JSON . stringify ( mockImdsCreds ) ) ;
66
+ it ( "gets token and profile name to fetch credentials" , async ( ) => {
67
+ ( httpRequest as jest . Mock )
68
+ . mockResolvedValueOnce ( mockToken )
69
+ . mockResolvedValueOnce ( mockProfile )
70
+ . mockResolvedValueOnce ( JSON . stringify ( mockImdsCreds ) ) ;
53
71
54
72
( retry as jest . Mock ) . mockImplementation ( ( fn : any ) => fn ( ) ) ;
55
73
( fromImdsCredentials as jest . Mock ) . mockReturnValue ( mockCreds ) ;
56
74
57
75
await expect ( fromInstanceMetadata ( ) ( ) ) . resolves . toEqual ( mockCreds ) ;
58
- expect ( httpRequest ) . toHaveBeenCalledTimes ( 2 ) ;
59
- expect ( httpRequest ) . toHaveBeenNthCalledWith ( 1 , mockHttpRequestOptions ) ;
60
- expect ( httpRequest ) . toHaveBeenNthCalledWith ( 2 , {
61
- ...mockHttpRequestOptions ,
62
- path : `${ mockHttpRequestOptions . path } ${ mockProfile } ` ,
76
+ expect ( httpRequest ) . toHaveBeenCalledTimes ( 3 ) ;
77
+ expect ( httpRequest ) . toHaveBeenNthCalledWith ( 1 , mockTokenRequestOptions ) ;
78
+ expect ( httpRequest ) . toHaveBeenNthCalledWith ( 2 , mockProfileRequestOptions ) ;
79
+ expect ( httpRequest ) . toHaveBeenNthCalledWith ( 3 , {
80
+ ...mockProfileRequestOptions ,
81
+ path : `${ mockProfileRequestOptions . path } ${ mockProfile } ` ,
63
82
} ) ;
64
83
} ) ;
65
84
66
85
it ( "trims profile returned name from IMDS" , async ( ) => {
67
86
( httpRequest as jest . Mock )
87
+ . mockResolvedValueOnce ( mockToken )
68
88
. mockResolvedValueOnce ( " " + mockProfile + " " )
69
89
. mockResolvedValueOnce ( JSON . stringify ( mockImdsCreds ) ) ;
70
90
71
91
( retry as jest . Mock ) . mockImplementation ( ( fn : any ) => fn ( ) ) ;
72
92
( fromImdsCredentials as jest . Mock ) . mockReturnValue ( mockCreds ) ;
73
93
74
94
await expect ( fromInstanceMetadata ( ) ( ) ) . resolves . toEqual ( mockCreds ) ;
75
- expect ( httpRequest ) . toHaveBeenCalledTimes ( 2 ) ;
76
- expect ( httpRequest ) . toHaveBeenNthCalledWith ( 1 , mockHttpRequestOptions ) ;
77
- expect ( httpRequest ) . toHaveBeenNthCalledWith ( 2 , {
78
- ...mockHttpRequestOptions ,
79
- path : `${ mockHttpRequestOptions . path } ${ mockProfile } ` ,
95
+ expect ( httpRequest ) . toHaveBeenNthCalledWith ( 3 , {
96
+ ...mockProfileRequestOptions ,
97
+ path : `${ mockProfileRequestOptions . path } ${ mockProfile } ` ,
80
98
} ) ;
81
99
} ) ;
82
100
@@ -107,7 +125,10 @@ describe("fromInstanceMetadata", () => {
107
125
} ) ;
108
126
109
127
it ( "throws ProviderError if credentials returned are incorrect" , async ( ) => {
110
- ( httpRequest as jest . Mock ) . mockResolvedValueOnce ( mockProfile ) . mockResolvedValueOnce ( JSON . stringify ( mockImdsCreds ) ) ;
128
+ ( httpRequest as jest . Mock )
129
+ . mockResolvedValueOnce ( mockToken )
130
+ . mockResolvedValueOnce ( mockProfile )
131
+ . mockResolvedValueOnce ( JSON . stringify ( mockImdsCreds ) ) ;
111
132
112
133
( retry as jest . Mock ) . mockImplementation ( ( fn : any ) => fn ( ) ) ;
113
134
( ( isImdsCredentials as unknown ) as jest . Mock ) . mockReturnValueOnce ( false ) ;
@@ -116,40 +137,175 @@ describe("fromInstanceMetadata", () => {
116
137
new ProviderError ( "Invalid response received from instance metadata service." )
117
138
) ;
118
139
expect ( retry ) . toHaveBeenCalledTimes ( 2 ) ;
119
- expect ( httpRequest ) . toHaveBeenCalledTimes ( 2 ) ;
140
+ expect ( httpRequest ) . toHaveBeenCalledTimes ( 3 ) ;
120
141
expect ( isImdsCredentials ) . toHaveBeenCalledTimes ( 1 ) ;
121
142
expect ( isImdsCredentials ) . toHaveBeenCalledWith ( mockImdsCreds ) ;
122
143
expect ( fromImdsCredentials ) . not . toHaveBeenCalled ( ) ;
123
144
} ) ;
124
145
125
- it ( "throws Error if requestFromEc2Imds for profile fails" , async ( ) => {
146
+ it ( "throws Error if httpRequest for profile fails" , async ( ) => {
126
147
const mockError = new Error ( "profile not found" ) ;
127
- ( httpRequest as jest . Mock ) . mockRejectedValueOnce ( mockError ) ;
148
+ ( httpRequest as jest . Mock ) . mockResolvedValueOnce ( mockToken ) . mockRejectedValueOnce ( mockError ) ;
128
149
( retry as jest . Mock ) . mockImplementation ( ( fn : any ) => fn ( ) ) ;
129
150
130
151
await expect ( fromInstanceMetadata ( ) ( ) ) . rejects . toEqual ( mockError ) ;
131
152
expect ( retry ) . toHaveBeenCalledTimes ( 1 ) ;
132
- expect ( httpRequest ) . toHaveBeenCalledTimes ( 1 ) ;
153
+ expect ( httpRequest ) . toHaveBeenCalledTimes ( 2 ) ;
133
154
} ) ;
134
155
135
- it ( "throws Error if requestFromEc2Imds for credentials fails" , async ( ) => {
156
+ it ( "throws Error if httpRequest for credentials fails" , async ( ) => {
136
157
const mockError = new Error ( "creds not found" ) ;
137
- ( httpRequest as jest . Mock ) . mockResolvedValueOnce ( mockProfile ) . mockRejectedValueOnce ( mockError ) ;
158
+ ( httpRequest as jest . Mock )
159
+ . mockResolvedValueOnce ( mockToken )
160
+ . mockResolvedValueOnce ( mockProfile )
161
+ . mockRejectedValueOnce ( mockError ) ;
138
162
( retry as jest . Mock ) . mockImplementation ( ( fn : any ) => fn ( ) ) ;
139
163
140
164
await expect ( fromInstanceMetadata ( ) ( ) ) . rejects . toEqual ( mockError ) ;
141
165
expect ( retry ) . toHaveBeenCalledTimes ( 2 ) ;
142
- expect ( httpRequest ) . toHaveBeenCalledTimes ( 2 ) ;
166
+ expect ( httpRequest ) . toHaveBeenCalledTimes ( 3 ) ;
143
167
expect ( fromImdsCredentials ) . not . toHaveBeenCalled ( ) ;
144
168
} ) ;
145
169
146
- it ( "throws SyntaxError if requestFromEc2Imds returns unparseable creds" , async ( ) => {
147
- ( httpRequest as jest . Mock ) . mockResolvedValueOnce ( mockProfile ) . mockResolvedValueOnce ( "." ) ;
170
+ it ( "throws SyntaxError if httpRequest returns unparseable creds" , async ( ) => {
171
+ ( httpRequest as jest . Mock )
172
+ . mockResolvedValueOnce ( mockToken )
173
+ . mockResolvedValueOnce ( mockProfile )
174
+ . mockResolvedValueOnce ( "." ) ;
148
175
( retry as jest . Mock ) . mockImplementation ( ( fn : any ) => fn ( ) ) ;
149
176
150
177
await expect ( fromInstanceMetadata ( ) ( ) ) . rejects . toEqual ( new SyntaxError ( "Unexpected token . in JSON at position 0" ) ) ;
151
178
expect ( retry ) . toHaveBeenCalledTimes ( 2 ) ;
152
- expect ( httpRequest ) . toHaveBeenCalledTimes ( 2 ) ;
179
+ expect ( httpRequest ) . toHaveBeenCalledTimes ( 3 ) ;
153
180
expect ( fromImdsCredentials ) . not . toHaveBeenCalled ( ) ;
154
181
} ) ;
182
+
183
+ it ( "throws error if metadata token errors with statusCode 400" , async ( ) => {
184
+ const tokenError = Object . assign ( new Error ( "token not found" ) , {
185
+ statusCode : 400 ,
186
+ } ) ;
187
+ ( httpRequest as jest . Mock ) . mockRejectedValueOnce ( tokenError ) ;
188
+
189
+ await expect ( fromInstanceMetadata ( ) ( ) ) . rejects . toEqual ( tokenError ) ;
190
+ } ) ;
191
+
192
+ describe ( "disables fetching of token" , ( ) => {
193
+ beforeEach ( ( ) => {
194
+ ( retry as jest . Mock ) . mockImplementation ( ( fn : any ) => fn ( ) ) ;
195
+ ( fromImdsCredentials as jest . Mock ) . mockReturnValue ( mockCreds ) ;
196
+ } ) ;
197
+
198
+ it ( "when token fetch returns with TimeoutError" , async ( ) => {
199
+ const tokenError = new Error ( "TimeoutError" ) ;
200
+
201
+ ( httpRequest as jest . Mock )
202
+ . mockRejectedValueOnce ( tokenError )
203
+ . mockResolvedValueOnce ( mockProfile )
204
+ . mockResolvedValueOnce ( JSON . stringify ( mockImdsCreds ) )
205
+ . mockResolvedValueOnce ( mockProfile )
206
+ . mockResolvedValueOnce ( JSON . stringify ( mockImdsCreds ) ) ;
207
+
208
+ const fromInstanceMetadataFunc = fromInstanceMetadata ( ) ;
209
+ await expect ( fromInstanceMetadataFunc ( ) ) . resolves . toEqual ( mockCreds ) ;
210
+ await expect ( fromInstanceMetadataFunc ( ) ) . resolves . toEqual ( mockCreds ) ;
211
+ } ) ;
212
+
213
+ [ 403 , 404 , 405 ] . forEach ( ( statusCode ) => {
214
+ it ( `when token fetch errors with statusCode ${ statusCode } ` , async ( ) => {
215
+ const tokenError = Object . assign ( new Error ( ) , { statusCode } ) ;
216
+
217
+ ( httpRequest as jest . Mock )
218
+ . mockRejectedValueOnce ( tokenError )
219
+ . mockResolvedValueOnce ( mockProfile )
220
+ . mockResolvedValueOnce ( JSON . stringify ( mockImdsCreds ) )
221
+ . mockResolvedValueOnce ( mockProfile )
222
+ . mockResolvedValueOnce ( JSON . stringify ( mockImdsCreds ) ) ;
223
+
224
+ const fromInstanceMetadataFunc = fromInstanceMetadata ( ) ;
225
+ await expect ( fromInstanceMetadataFunc ( ) ) . resolves . toEqual ( mockCreds ) ;
226
+ await expect ( fromInstanceMetadataFunc ( ) ) . resolves . toEqual ( mockCreds ) ;
227
+ } ) ;
228
+ } ) ;
229
+ } ) ;
230
+
231
+ it ( "uses insecure data flow once, if error is not TimeoutError" , async ( ) => {
232
+ const tokenError = new Error ( "Error" ) ;
233
+
234
+ ( httpRequest as jest . Mock )
235
+ . mockRejectedValueOnce ( tokenError )
236
+ . mockResolvedValueOnce ( mockProfile )
237
+ . mockResolvedValueOnce ( JSON . stringify ( mockImdsCreds ) )
238
+ . mockResolvedValueOnce ( mockToken )
239
+ . mockResolvedValueOnce ( mockProfile )
240
+ . mockResolvedValueOnce ( JSON . stringify ( mockImdsCreds ) ) ;
241
+
242
+ ( retry as jest . Mock ) . mockImplementation ( ( fn : any ) => fn ( ) ) ;
243
+ ( fromImdsCredentials as jest . Mock ) . mockReturnValue ( mockCreds ) ;
244
+
245
+ const fromInstanceMetadataFunc = fromInstanceMetadata ( ) ;
246
+ await expect ( fromInstanceMetadataFunc ( ) ) . resolves . toEqual ( mockCreds ) ;
247
+ await expect ( fromInstanceMetadataFunc ( ) ) . resolves . toEqual ( mockCreds ) ;
248
+ } ) ;
249
+
250
+ it ( "uses insecure data flow once, if error statusCode is not 400, 403, 404, 405" , async ( ) => {
251
+ const tokenError = Object . assign ( new Error ( "Error" ) , { statusCode : 406 } ) ;
252
+
253
+ ( httpRequest as jest . Mock )
254
+ . mockRejectedValueOnce ( tokenError )
255
+ . mockResolvedValueOnce ( mockProfile )
256
+ . mockResolvedValueOnce ( JSON . stringify ( mockImdsCreds ) )
257
+ . mockResolvedValueOnce ( mockToken )
258
+ . mockResolvedValueOnce ( mockProfile )
259
+ . mockResolvedValueOnce ( JSON . stringify ( mockImdsCreds ) ) ;
260
+
261
+ ( retry as jest . Mock ) . mockImplementation ( ( fn : any ) => fn ( ) ) ;
262
+ ( fromImdsCredentials as jest . Mock ) . mockReturnValue ( mockCreds ) ;
263
+
264
+ const fromInstanceMetadataFunc = fromInstanceMetadata ( ) ;
265
+ await expect ( fromInstanceMetadataFunc ( ) ) . resolves . toEqual ( mockCreds ) ;
266
+ await expect ( fromInstanceMetadataFunc ( ) ) . resolves . toEqual ( mockCreds ) ;
267
+ } ) ;
268
+
269
+ describe ( "re-enables fetching of token" , ( ) => {
270
+ const error401 = Object . assign ( new Error ( "error" ) , { statusCode : 401 } ) ;
271
+
272
+ beforeEach ( ( ) => {
273
+ const tokenError = new Error ( "TimeoutError" ) ;
274
+
275
+ ( httpRequest as jest . Mock )
276
+ . mockRejectedValueOnce ( tokenError )
277
+ . mockResolvedValueOnce ( mockProfile )
278
+ . mockResolvedValueOnce ( JSON . stringify ( mockImdsCreds ) ) ;
279
+
280
+ ( retry as jest . Mock ) . mockImplementation ( ( fn : any ) => fn ( ) ) ;
281
+ ( fromImdsCredentials as jest . Mock ) . mockReturnValue ( mockCreds ) ;
282
+ } ) ;
283
+
284
+ it ( "when profile error with 401" , async ( ) => {
285
+ ( httpRequest as jest . Mock )
286
+ . mockRejectedValueOnce ( error401 )
287
+ . mockResolvedValueOnce ( mockToken )
288
+ . mockResolvedValueOnce ( mockProfile )
289
+ . mockResolvedValueOnce ( JSON . stringify ( mockImdsCreds ) ) ;
290
+
291
+ const fromInstanceMetadataFunc = fromInstanceMetadata ( ) ;
292
+ await expect ( fromInstanceMetadataFunc ( ) ) . resolves . toEqual ( mockCreds ) ;
293
+ await expect ( fromInstanceMetadataFunc ( ) ) . rejects . toEqual ( error401 ) ;
294
+ await expect ( fromInstanceMetadataFunc ( ) ) . resolves . toEqual ( mockCreds ) ;
295
+ } ) ;
296
+
297
+ it ( "when creds error with 401" , async ( ) => {
298
+ ( httpRequest as jest . Mock )
299
+ . mockResolvedValueOnce ( mockProfile )
300
+ . mockRejectedValueOnce ( error401 )
301
+ . mockResolvedValueOnce ( mockToken )
302
+ . mockResolvedValueOnce ( mockProfile )
303
+ . mockResolvedValueOnce ( JSON . stringify ( mockImdsCreds ) ) ;
304
+
305
+ const fromInstanceMetadataFunc = fromInstanceMetadata ( ) ;
306
+ await expect ( fromInstanceMetadataFunc ( ) ) . resolves . toEqual ( mockCreds ) ;
307
+ await expect ( fromInstanceMetadataFunc ( ) ) . rejects . toEqual ( error401 ) ;
308
+ await expect ( fromInstanceMetadataFunc ( ) ) . resolves . toEqual ( mockCreds ) ;
309
+ } ) ;
310
+ } ) ;
155
311
} ) ;
0 commit comments