@@ -6,8 +6,13 @@ const withV4 = require('../support/withV4');
66const BucketUtility = require ( '../../lib/utility/bucket-util' ) ;
77const bucketSchema = require ( '../../schema/bucket' ) ;
88const bucketSchemaV2 = require ( '../../schema/bucketV2' ) ;
9- const { generateToken, decryptToken } =
10- require ( '../../../../../lib/api/apiUtils/object/continueToken' ) ;
9+ const { generateToken, decryptToken } = require ( '../../../../../lib/api/apiUtils/object/continueToken' ) ;
10+ const AWS = require ( 'aws-sdk' ) ;
11+ const { IAM } = AWS ;
12+ const getConfig = require ( '../support/config' ) ;
13+ const { config } = require ( '../../../../../lib/Config' ) ;
14+
15+ const vaultHost = config . vaultd ?. host || 'localhost' ;
1116
1217const tests = [
1318 {
@@ -374,64 +379,6 @@ describe('GET Bucket - AWS.S3.listObjects', () => {
374379 } ) ;
375380 } ) ;
376381
377- it ( 'should manage the x-amz-optional-attributes header' , async ( ) => {
378- const s3 = bucketUtil . s3 ;
379- const Bucket = bucketName ;
380-
381- await s3 . putObject ( {
382- Bucket,
383- Key : 'super-power-object' ,
384- Metadata : {
385- Department : 'sales' ,
386- HR : 'true' ,
387- } ,
388- } ) . promise ( ) ;
389-
390- const result = await new Promise ( ( resolve , reject ) => {
391- let rawXml = '' ;
392- const req = s3 . listObjectsV2 ( { Bucket } ) ;
393-
394- req . on ( 'build' , ( ) => {
395- req . httpRequest . headers [ 'x-amz-optional-object-attributes' ] = 'x-amz-meta-*' ;
396- req . httpRequest . headers [ 'x-amz-optional-object-attributes' ] += ',RestoreStatus' ;
397- req . httpRequest . headers [ 'x-amz-optional-object-attributes' ] += ',x-amz-meta-department' ;
398- } ) ;
399- req . on ( 'httpData' , chunk => { rawXml += chunk ; } ) ;
400- req . on ( 'error' , err => reject ( err ) ) ;
401- req . on ( 'success' , response => {
402- parseString ( rawXml , ( err , parsedXml ) => {
403- if ( err ) {
404- return reject ( err ) ;
405- }
406-
407- const contents = response . data . Contents ;
408- const parsedContents = parsedXml . ListBucketResult . Contents ;
409-
410- if ( ! contents || ! parsedContents ) {
411- return resolve ( response . data ) ;
412- }
413-
414- if ( parsedContents [ 0 ] ?. [ 'x-amz-meta-department' ] ) {
415- contents [ 0 ] [ 'x-amz-meta-department' ] = parsedContents [ 0 ] [ 'x-amz-meta-department' ] [ 0 ] ;
416- }
417-
418- if ( parsedContents [ 0 ] ?. [ 'x-amz-meta-hr' ] ) {
419- contents [ 0 ] [ 'x-amz-meta-hr' ] = parsedContents [ 0 ] [ 'x-amz-meta-hr' ] [ 0 ] ;
420- }
421-
422- return resolve ( response . data ) ;
423- } ) ;
424- } ) ;
425-
426- req . send ( ) ;
427- } ) ;
428-
429- assert . strictEqual ( result . Contents . length , 1 ) ;
430- assert . strictEqual ( result . Contents [ 0 ] . Key , 'super-power-object' ) ;
431- assert . strictEqual ( result . Contents [ 0 ] [ 'x-amz-meta-department' ] , 'sales' ) ;
432- assert . strictEqual ( result . Contents [ 0 ] [ 'x-amz-meta-hr' ] , 'true' ) ;
433- } ) ;
434-
435382 [ '&' , '"quot' , '\'apos' , '<lt' , '>gt' ] . forEach ( k => {
436383 it ( `should list objects with key ${ k } as Prefix` , async ( ) => {
437384 const s3 = bucketUtil . s3 ;
@@ -549,5 +496,175 @@ describe('GET Bucket - AWS.S3.listObjects', () => {
549496 decryptToken ( data . NextContinuationToken ) , k ) ;
550497 } ) ;
551498 } ) ;
499+
500+ describe ( 'x-amz-optional-attributes header' , ( ) => {
501+ let policyWithoutPermission ;
502+ let userWithoutPermission ;
503+ let s3ClientWithoutPermission ;
504+
505+ const iamConfig = getConfig ( 'default' , { region : 'us-east-1' } ) ;
506+ iamConfig . endpoint = `http://${ vaultHost } :8600` ;
507+ const iamClient = new IAM ( iamConfig ) ;
508+
509+ before ( async ( ) => {
510+ const policyRes = await iamClient
511+ . createPolicy ( {
512+ PolicyName : 'bp-bypass-policy' ,
513+ PolicyDocument : JSON . stringify ( {
514+ Version : '2012-10-17' ,
515+ Statement : [ {
516+ Sid : 'AllowS3ListBucket' ,
517+ Effect : 'Allow' ,
518+ Action : [
519+ 's3:ListBucket' ,
520+ ] ,
521+ Resource : [ '*' ] ,
522+ } ] ,
523+ } ) ,
524+ } )
525+ . promise ( ) ;
526+ policyWithoutPermission = policyRes . Policy ;
527+ const userRes = await iamClient . createUser ( { UserName : 'user-without-permission' } ) . promise ( ) ;
528+ userWithoutPermission = userRes . User ;
529+ await iamClient
530+ . attachUserPolicy ( {
531+ UserName : userWithoutPermission . UserName ,
532+ PolicyArn : policyWithoutPermission . Arn ,
533+ } )
534+ . promise ( ) ;
535+
536+ const accessKeyRes = await iamClient . createAccessKey ( {
537+ UserName : userWithoutPermission . UserName ,
538+ } ) . promise ( ) ;
539+ const accessKey = accessKeyRes . AccessKey ;
540+ const s3Config = getConfig ( 'default' , {
541+ credentials : new AWS . Credentials ( accessKey . AccessKeyId , accessKey . SecretAccessKey ) ,
542+ } ) ;
543+ s3ClientWithoutPermission = new AWS . S3 ( s3Config ) ;
544+ } ) ;
545+
546+ after ( async ( ) => {
547+ await iamClient
548+ . detachUserPolicy ( {
549+ UserName : userWithoutPermission . UserName ,
550+ PolicyArn : policyWithoutPermission . Arn ,
551+ } )
552+ . promise ( ) ;
553+ await iamClient . deletePolicy ( { PolicyArn : policyWithoutPermission . Arn } ) . promise ( ) ;
554+ await iamClient . deleteUser ( { UserName : userWithoutPermission . UserName } ) . promise ( ) ;
555+ } ) ;
556+
557+ // eslint-disable-next-line max-len
558+ const listObjectsV2WithOptionalAttributes = async ( s3 , bucket , headerValue ) => await new Promise ( ( resolve , reject ) => {
559+ let rawXml = '' ;
560+ const req = s3 . listObjectsV2 ( { Bucket : bucket } ) ;
561+
562+ req . on ( 'build' , ( ) => {
563+ req . httpRequest . headers [ 'x-amz-optional-object-attributes' ] = headerValue ;
564+ } ) ;
565+ req . on ( 'httpData' , chunk => { rawXml += chunk ; } ) ;
566+ req . on ( 'error' , err => reject ( err ) ) ;
567+ req . on ( 'success' , response => {
568+ parseString ( rawXml , ( err , parsedXml ) => {
569+ if ( err ) {
570+ return reject ( err ) ;
571+ }
572+
573+ const contents = response . data . Contents ;
574+ const parsedContents = parsedXml . ListBucketResult . Contents ;
575+
576+ if ( ! contents || ! parsedContents ) {
577+ return resolve ( response . data ) ;
578+ }
579+
580+ if ( parsedContents [ 0 ] ?. [ 'x-amz-meta-department' ] ) {
581+ contents [ 0 ] [ 'x-amz-meta-department' ] = parsedContents [ 0 ] [ 'x-amz-meta-department' ] [ 0 ] ;
582+ }
583+
584+ if ( parsedContents [ 0 ] ?. [ 'x-amz-meta-hr' ] ) {
585+ contents [ 0 ] [ 'x-amz-meta-hr' ] = parsedContents [ 0 ] [ 'x-amz-meta-hr' ] [ 0 ] ;
586+ }
587+
588+ return resolve ( response . data ) ;
589+ } ) ;
590+ } ) ;
591+
592+ req . send ( ) ;
593+ } ) ;
594+
595+ it ( 'should return an XML if the header is set' , async ( ) => {
596+ const s3 = bucketUtil . s3 ;
597+ const Bucket = bucketName ;
598+
599+ await s3 . putObject ( {
600+ Bucket,
601+ Key : 'super-power-object' ,
602+ Metadata : {
603+ Department : 'sales' ,
604+ HR : 'true' ,
605+ } ,
606+ } ) . promise ( ) ;
607+ const result = await listObjectsV2WithOptionalAttributes (
608+ s3 ,
609+ Bucket ,
610+ 'x-amz-meta-*,RestoreStatus,x-amz-meta-department' ,
611+ ) ;
612+
613+ assert . strictEqual ( result . Contents . length , 1 ) ;
614+ assert . strictEqual ( result . Contents [ 0 ] . Key , 'super-power-object' ) ;
615+ assert . strictEqual ( result . Contents [ 0 ] [ 'x-amz-meta-department' ] , 'sales' ) ;
616+ assert . strictEqual ( result . Contents [ 0 ] [ 'x-amz-meta-hr' ] , 'true' ) ;
617+ } ) ;
618+
619+ it ( 'should reject the request if the user does not have the permission' , async ( ) => {
620+ const s3 = bucketUtil . s3 ;
621+ const Bucket = bucketName ;
622+
623+ await s3 . putObject ( {
624+ Bucket,
625+ Key : 'super-power-object' ,
626+ Metadata : {
627+ Department : 'sales' ,
628+ HR : 'true' ,
629+ } ,
630+ } ) . promise ( ) ;
631+
632+ try {
633+ const result = await listObjectsV2WithOptionalAttributes (
634+ s3ClientWithoutPermission ,
635+ Bucket ,
636+ 'x-amz-meta-*,RestoreStatus,x-amz-meta-department' ,
637+ ) ;
638+ throw new Error ( 'Request should have been rejected' ) ;
639+ } catch ( err ) {
640+ assert . strictEqual ( err . statusCode , 403 ) ;
641+ assert . strictEqual ( err . code , 'AccessDenied' ) ;
642+ }
643+ } ) ;
644+
645+ it ( 'should return an XML if the header is only RestoreStatus even without permission' , async ( ) => {
646+ const s3 = bucketUtil . s3 ;
647+ const Bucket = bucketName ;
648+
649+ await s3 . putObject ( {
650+ Bucket,
651+ Key : 'super-power-object' ,
652+ Metadata : {
653+ Department : 'sales' ,
654+ HR : 'true' ,
655+ } ,
656+ } ) . promise ( ) ;
657+ const result = await listObjectsV2WithOptionalAttributes (
658+ s3ClientWithoutPermission ,
659+ Bucket ,
660+ 'RestoreStatus' ,
661+ ) ;
662+
663+ assert . strictEqual ( result . Contents . length , 1 ) ;
664+ assert . strictEqual ( result . Contents [ 0 ] . Key , 'super-power-object' ) ;
665+ assert . strictEqual ( result . Contents [ 0 ] [ 'x-amz-meta-department' ] , undefined ) ;
666+ assert . strictEqual ( result . Contents [ 0 ] [ 'x-amz-meta-hr' ] , undefined ) ;
667+ } ) ;
668+ } ) ;
552669 } ) ;
553670} ) ;
0 commit comments