@@ -117,7 +117,7 @@ function checkBucketAcls(bucket, requestType, canonicalID, mainApiCall) {
117
117
// authorization check should just return true so can move on to check
118
118
// rights at the object level.
119
119
return ( requestTypeParsed === 'objectPutACL' || requestTypeParsed === 'objectGetACL'
120
- || requestTypeParsed === 'objectGet' || requestTypeParsed === 'objectHead' ) ;
120
+ || requestTypeParsed === 'objectGet' || requestTypeParsed === 'objectHead' ) ;
121
121
}
122
122
123
123
function checkObjectAcls ( bucket , objectMD , requestType , canonicalID , requesterIsNotUser ,
@@ -265,67 +265,207 @@ function _isAccountId(principal) {
265
265
return ( principal . length === 12 && / ^ \d + $ / . test ( principal ) ) ;
266
266
}
267
267
268
- function _checkPrincipal ( requester , principal ) {
269
- if ( principal === '*' ) {
270
- return true ;
271
- }
272
- // User in unauthenticated (anonymous request)
273
- if ( requester === undefined ) {
268
+ function _isRootUser ( arn ) {
269
+ if ( ! arn ) {
274
270
return false ;
275
271
}
276
- if ( principal === requester ) {
272
+
273
+ // Vault returns the following arn when the account makes requests 'arn:aws:iam::295412255825:/master/',
274
+ // with an empty resource type ('user/' prefix missing).
275
+ const arns = arn . split ( ':' ) ;
276
+ const resource = arns . at ( - 1 ) ;
277
+
278
+ // If we start with '/' is because we have a empty resource type so we know it is a root account.
279
+ if ( resource . startsWith ( '/' ) ) {
277
280
return true ;
278
281
}
279
- if ( _isAccountId ( principal ) ) {
280
- return _getAccountId ( requester ) === principal ;
282
+
283
+ return false ;
284
+ }
285
+
286
+ const checkPrincipalResult = Object . freeze ( {
287
+ KO : 0 ,
288
+ CROSS_ACCOUNT_OK : 1 ,
289
+ OK : 2 ,
290
+ } ) ;
291
+
292
+ /** _evaluateCrossAccount - checks if it is a cross-account request.
293
+ * @param {string } requesterARN - requester ARN
294
+ * @param {string } requesterCanonicalID - requester canonical ID
295
+ * @param {string } bucketOwnerCanonicalID - bucket owner canonical ID
296
+ * @return {checkPrincipalResult } OK if it is not cross-account, CROSS_ACCOUNT_OK otherwise.
297
+ */
298
+ function _checkCrossAccount ( requesterARN , requesterCanonicalID , bucketOwnerCanonicalID ) {
299
+ if ( ! _isRootUser ( requesterARN ) ) {
300
+ return bucketOwnerCanonicalID === requesterCanonicalID ?
301
+ checkPrincipalResult . OK : checkPrincipalResult . CROSS_ACCOUNT_OK ;
281
302
}
282
- if ( principal . endsWith ( 'root' ) ) {
283
- return _getAccountId ( requester ) === _getAccountId ( principal ) ;
303
+
304
+ return checkPrincipalResult . OK ;
305
+ }
306
+
307
+ function _checkPrincipalWildcard ( requestARN , requesterCanonicalID , bucketOwnerCanonicalID ) {
308
+ if ( requestARN === undefined ) { // User in unauthenticated (anonymous request)
309
+ return checkPrincipalResult . OK ;
284
310
}
285
- return false ;
311
+
312
+ return _checkCrossAccount ( requestARN , requesterCanonicalID , bucketOwnerCanonicalID ) ;
286
313
}
287
314
288
- function _checkPrincipals ( canonicalID , arn , principal ) {
315
+ function _checkPrincipalAWS ( principal , requesterARN , requesterCanonicalID , bucketOwnerCanonicalID ) {
289
316
if ( principal === '*' ) {
290
- return true ;
317
+ return _checkPrincipalWildcard ( requesterARN , requesterCanonicalID , bucketOwnerCanonicalID ) ;
291
318
}
292
- if ( principal . CanonicalUser ) {
293
- if ( Array . isArray ( principal . CanonicalUser ) ) {
294
- return principal . CanonicalUser . some ( p => _checkPrincipal ( canonicalID , p ) ) ;
319
+
320
+ if ( requesterARN === undefined ) { // User in unauthenticated (anonymous request)
321
+ return checkPrincipalResult . KO ;
322
+ }
323
+
324
+ if ( principal === requesterARN ) {
325
+ return _checkCrossAccount ( requesterARN , requesterCanonicalID , bucketOwnerCanonicalID ) ;
326
+ }
327
+
328
+ if ( _isAccountId ( principal ) && principal === _getAccountId ( requesterARN ) ) {
329
+ return _checkCrossAccount ( requesterARN , requesterCanonicalID , bucketOwnerCanonicalID ) ;
330
+ }
331
+
332
+ if ( principal . endsWith ( 'root' ) && _getAccountId ( principal ) === _getAccountId ( requesterARN ) ) {
333
+ return _checkCrossAccount ( requesterARN , requesterCanonicalID , bucketOwnerCanonicalID ) ;
334
+ }
335
+
336
+ return checkPrincipalResult . KO ;
337
+ }
338
+
339
+ function _checkPrincipalCanonicalUser ( principal , requesterARN , requesterCanonicalID , bucketOwnerCanonicalID ) {
340
+ if ( principal === '*' ) {
341
+ return _checkPrincipalWildcard ( requesterARN , requesterCanonicalID , bucketOwnerCanonicalID ) ;
342
+ }
343
+
344
+ if ( requesterARN === undefined ) { // User in unauthenticated (anonymous request)
345
+ return checkPrincipalResult . KO ;
346
+ }
347
+
348
+ if ( principal === requesterCanonicalID ) {
349
+ return _checkCrossAccount ( requesterARN , requesterCanonicalID , bucketOwnerCanonicalID ) ;
350
+ }
351
+
352
+ return checkPrincipalResult . KO ;
353
+ }
354
+
355
+ function _findBestPrincipalMatch ( principalArray , checkFunc ) {
356
+ let bestMatch = checkPrincipalResult . KO ;
357
+ if ( ! principalArray ) {
358
+ return bestMatch ;
359
+ }
360
+
361
+ const principals = Array . isArray ( principalArray ) ? principalArray : [ principalArray ] ;
362
+
363
+ for ( const p of principals ) {
364
+ const result = checkFunc ( p ) ;
365
+ if ( result === checkPrincipalResult . OK ) {
366
+ return checkPrincipalResult . OK ; // Highest permission, can exit early
367
+ }
368
+ if ( result > bestMatch ) {
369
+ bestMatch = result ;
295
370
}
296
- return _checkPrincipal ( canonicalID , principal . CanonicalUser ) ;
297
371
}
372
+
373
+ return bestMatch ;
374
+ }
375
+
376
+ function _checkPrincipals ( canonicalID , arn , principal , bucketOwnerCanonicalID ) {
377
+ if ( principal === '*' ) {
378
+ return _checkPrincipalWildcard ( arn , canonicalID , bucketOwnerCanonicalID ) ;
379
+ }
380
+
381
+ if ( principal . CanonicalUser ) {
382
+ return _findBestPrincipalMatch ( principal . CanonicalUser ,
383
+ p => _checkPrincipalCanonicalUser ( p , arn , canonicalID , bucketOwnerCanonicalID ) ) ;
384
+ }
385
+
298
386
if ( principal . AWS ) {
299
- if ( Array . isArray ( principal . AWS ) ) {
300
- return principal . AWS . some ( p => _checkPrincipal ( arn , p ) ) ;
301
- }
302
- return _checkPrincipal ( arn , principal . AWS ) ;
387
+ return _findBestPrincipalMatch ( principal . AWS ,
388
+ p => _checkPrincipalAWS ( p , arn , canonicalID , bucketOwnerCanonicalID ) ) ;
303
389
}
304
- return false ;
390
+
391
+ return checkPrincipalResult . KO ;
305
392
}
306
393
394
+ const checkBucketPolicyResult = Object . freeze ( {
395
+ DEFAULT_DENY : 0 ,
396
+ EXPLICIT_DENY : 1 ,
397
+ ALLOW : 2 ,
398
+ CROSS_ACCOUNT_ALLOW : 3 ,
399
+ } ) ;
400
+
401
+ // checkBucketPolicy FSM.
402
+ // ┌───────────────────────────┐
403
+ // │ ▼
404
+ // │ ┌───────┐
405
+ // │ ┌────────►│ ALLOW ├──────────────┐
406
+ // │ ┌─────┐ │ └──┬────┘ │
407
+ // │ │START│ │ │ │
408
+ // │ └──┬──┘ │ │ │
409
+ // │ │ │ │ │
410
+ // │ ▼ │ ▼ ▼
411
+ // │┌──────────────┤ ┌────┐ ┌─────┐
412
+ // ││ DEFAULT_DENY ├─────────►│DENY├────────────►│ END │
413
+ // │└──────┬───────┤ └────┘ └─────┘
414
+ // │ │ │ ▲ ▲ ▲
415
+ // │ │ │ │ │ │
416
+ // │ │ │ │ │ │
417
+ // │ │ │ ┌───┴───────────┐ │ │
418
+ // │ │ └────────►│ CROSS_ACCOUNT ├──────┘ │
419
+ // │ │ └┬──────────────┘ │
420
+ // └───────┼──────────────────┘ │
421
+ // └──────────────────────────────────────────┘
422
+ //
307
423
function checkBucketPolicy ( policy , requestType , canonicalID , arn , bucketOwner , log , request , actionImplicitDenies ) {
308
- let permission = 'defaultDeny' ;
424
+ let permission = checkBucketPolicyResult . DEFAULT_DENY ;
309
425
// if requester is user within bucket owner account, actions should be
310
426
// allowed unless explicitly denied (assumes allowed by IAM policy)
311
427
if ( bucketOwner === canonicalID && actionImplicitDenies [ requestType ] === false ) {
312
- permission = 'allow' ;
428
+ permission = checkBucketPolicyResult . ALLOW ;
313
429
}
314
430
let copiedStatement = JSON . parse ( JSON . stringify ( policy . Statement ) ) ;
315
431
while ( copiedStatement . length > 0 ) {
316
432
const s = copiedStatement [ 0 ] ;
317
- const principalMatch = _checkPrincipals ( canonicalID , arn , s . Principal ) ;
433
+ const principalMatch = _checkPrincipals ( canonicalID , arn , s . Principal , bucketOwner ) ;
318
434
const actionMatch = _checkBucketPolicyActions ( requestType , s . Action , log ) ;
319
435
const resourceMatch = _checkBucketPolicyResources ( request , s . Resource , log ) ;
320
436
const conditionsMatch = _checkBucketPolicyConditions ( request , s . Condition , log ) ;
321
437
322
- if ( principalMatch && actionMatch && resourceMatch && conditionsMatch && s . Effect === 'Deny' ) {
323
- // explicit deny trumps any allows, so return immediately
324
- return 'explicitDeny' ;
325
- }
326
- if ( principalMatch && actionMatch && resourceMatch && conditionsMatch && s . Effect === 'Allow' ) {
327
- permission = 'allow' ;
438
+ const ok = principalMatch === checkPrincipalResult . OK && actionMatch && resourceMatch && conditionsMatch ;
439
+ const okCross = principalMatch === checkPrincipalResult . CROSS_ACCOUNT_OK
440
+ && actionMatch && resourceMatch && conditionsMatch ;
441
+ switch ( permission ) {
442
+ case checkBucketPolicyResult . DEFAULT_DENY :
443
+ if ( ( ok || okCross ) && s . Effect === 'Deny' ) {
444
+ return checkBucketPolicyResult . EXPLICIT_DENY ;
445
+ } else if ( ok && s . Effect === 'Allow' ) {
446
+ permission = checkBucketPolicyResult . ALLOW ;
447
+ } else if ( okCross && s . Effect === 'Allow' ) {
448
+ permission = checkBucketPolicyResult . CROSS_ACCOUNT_ALLOW ;
449
+ }
450
+ break ;
451
+ case checkBucketPolicyResult . EXPLICIT_DENY :
452
+ return checkBucketPolicyResult . EXPLICIT_DENY ;
453
+ case checkBucketPolicyResult . ALLOW :
454
+ if ( ( ok || okCross ) && s . Effect === 'Deny' ) {
455
+ return checkBucketPolicyResult . EXPLICIT_DENY ;
456
+ }
457
+ break ;
458
+ case checkBucketPolicyResult . CROSS_ACCOUNT_ALLOW :
459
+ if ( ( ok || okCross ) && s . Effect === 'Deny' ) {
460
+ return checkBucketPolicyResult . EXPLICIT_DENY ;
461
+ } else if ( ok && s . Effect === 'Allow' ) {
462
+ permission = checkBucketPolicyResult . ALLOW ;
463
+ }
464
+ break ;
465
+ default : // Needed for the linter, should be unreachable.
466
+ break ;
328
467
}
468
+
329
469
copiedStatement = copiedStatement . splice ( 1 ) ;
330
470
}
331
471
return permission ;
@@ -341,9 +481,13 @@ function processBucketPolicy(requestType, bucket, canonicalID, arn, bucketOwner,
341
481
const bucketPolicyPermission = checkBucketPolicy ( bucketPolicy , requestType , canonicalID , arn ,
342
482
bucketOwner , log , request , actionImplicitDenies ) ;
343
483
344
- if ( bucketPolicyPermission === 'explicitDeny' ) {
484
+ if ( bucketPolicyPermission === checkBucketPolicyResult . EXPLICIT_DENY ) {
345
485
processedResult = false ;
346
- } else if ( bucketPolicyPermission === 'allow' ) {
486
+ } else if ( bucketPolicyPermission === checkBucketPolicyResult . ALLOW ) {
487
+ processedResult = true ;
488
+ } else if ( bucketPolicyPermission === checkBucketPolicyResult . CROSS_ACCOUNT_ALLOW
489
+ && actionImplicitDenies [ requestType ] === false ) {
490
+ // If the bucket policy is cross account, only return true if Vault also returned an explicit allow.
347
491
processedResult = true ;
348
492
} else {
349
493
processedResult = actionImplicitDenies [ requestType ] === false && aclPermission ;
@@ -405,7 +549,7 @@ function evaluateBucketPolicyWithIAM(bucket, requestTypesInput, canonicalID, aut
405
549
arn = authInfo . getArn ( ) ;
406
550
}
407
551
return processBucketPolicy ( _requestType , bucket , canonicalID , arn , bucket . getOwner ( ) , log ,
408
- request , true , results , actionImplicitDenies ) ;
552
+ request , true , results , actionImplicitDenies ) ;
409
553
} ) ;
410
554
}
411
555
@@ -456,8 +600,8 @@ function isObjAuthorized(bucket, objectMD, requestTypesInput, canonicalID, authI
456
600
// - account is the bucket owner
457
601
// - requester is account, not user
458
602
if ( bucketOwnerActions . includes ( parsedMethodName )
459
- && ( bucketOwner === canonicalID )
460
- && requesterIsNotUser ) {
603
+ && ( bucketOwner === canonicalID )
604
+ && requesterIsNotUser ) {
461
605
results [ _requestType ] = actionImplicitDenies [ _requestType ] === false ;
462
606
return results [ _requestType ] ;
463
607
}
@@ -613,4 +757,6 @@ module.exports = {
613
757
validatePolicyConditions,
614
758
isLifecycleSession,
615
759
evaluateBucketPolicyWithIAM,
760
+ checkBucketPolicy,
761
+ checkBucketPolicyResult,
616
762
} ;
0 commit comments