@@ -5,13 +5,21 @@ const {
55 ListObjectsCommand,
66 ListObjectsV2Command,
77} = require ( '@aws-sdk/client-s3' ) ;
8+ const { parseString } = require ( 'xml2js' ) ;
89
910const withV4 = require ( '../support/withV4' ) ;
1011const BucketUtility = require ( '../../lib/utility/bucket-util' ) ;
1112const bucketSchema = require ( '../../schema/bucket' ) ;
1213const bucketSchemaV2 = require ( '../../schema/bucketV2' ) ;
13- const { generateToken, decryptToken } =
14- require ( '../../../../../lib/api/apiUtils/object/continueToken' ) ;
14+ const { generateToken, decryptToken } = require ( '../../../../../lib/api/apiUtils/object/continueToken' ) ;
15+ const AWS = require ( 'aws-sdk' ) ;
16+ const { IAM } = AWS ;
17+ const getConfig = require ( '../support/config' ) ;
18+ const { config } = require ( '../../../../../lib/Config' ) ;
19+
20+ const isVaultScality = config . backends . auth !== 'mem' ;
21+ const internalPortBypassBP = config . internalPort ;
22+ const vaultHost = config . vaultd ?. host || 'localhost' ;
1523
1624const tests = [
1725 {
@@ -535,5 +543,176 @@ describe('GET Bucket - AWS.S3.listObjects', () => {
535543 assert . strictEqual ( $metadata . httpStatusCode , 200 ) ;
536544 } ) ;
537545 } ) ;
546+
547+ const describeBypass = isVaultScality && internalPortBypassBP ? describe : describe . skip ;
548+ describeBypass ( 'x-amz-optional-attributes header' , ( ) => {
549+ let policyWithoutPermission ;
550+ let userWithoutPermission ;
551+ let s3ClientWithoutPermission ;
552+
553+ const iamConfig = getConfig ( 'default' , { region : 'us-east-1' } ) ;
554+ iamConfig . endpoint = `http://${ vaultHost } :8600` ;
555+ const iamClient = new IAM ( iamConfig ) ;
556+
557+ before ( async ( ) => {
558+ const policyRes = await iamClient
559+ . createPolicy ( {
560+ PolicyName : 'bp-bypass-policy' ,
561+ PolicyDocument : JSON . stringify ( {
562+ Version : '2012-10-17' ,
563+ Statement : [ {
564+ Sid : 'AllowS3ListBucket' ,
565+ Effect : 'Allow' ,
566+ Action : [
567+ 's3:ListBucket' ,
568+ ] ,
569+ Resource : [ '*' ] ,
570+ } ] ,
571+ } ) ,
572+ } )
573+ . promise ( ) ;
574+ policyWithoutPermission = policyRes . Policy ;
575+ const userRes = await iamClient . createUser ( { UserName : 'user-without-permission' } ) . promise ( ) ;
576+ userWithoutPermission = userRes . User ;
577+ await iamClient
578+ . attachUserPolicy ( {
579+ UserName : userWithoutPermission . UserName ,
580+ PolicyArn : policyWithoutPermission . Arn ,
581+ } )
582+ . promise ( ) ;
583+
584+ const accessKeyRes = await iamClient . createAccessKey ( {
585+ UserName : userWithoutPermission . UserName ,
586+ } ) . promise ( ) ;
587+ const accessKey = accessKeyRes . AccessKey ;
588+ const s3Config = getConfig ( 'default' , {
589+ credentials : new AWS . Credentials ( accessKey . AccessKeyId , accessKey . SecretAccessKey ) ,
590+ } ) ;
591+ s3ClientWithoutPermission = new AWS . S3 ( s3Config ) ;
592+ } ) ;
593+
594+ after ( async ( ) => {
595+ await iamClient
596+ . detachUserPolicy ( {
597+ UserName : userWithoutPermission . UserName ,
598+ PolicyArn : policyWithoutPermission . Arn ,
599+ } )
600+ . promise ( ) ;
601+ await iamClient . deletePolicy ( { PolicyArn : policyWithoutPermission . Arn } ) . promise ( ) ;
602+ await iamClient . deleteUser ( { UserName : userWithoutPermission . UserName } ) . promise ( ) ;
603+ } ) ;
604+
605+ // eslint-disable-next-line max-len
606+ const listObjectsV2WithOptionalAttributes = async ( s3 , bucket , headerValue ) => await new Promise ( ( resolve , reject ) => {
607+ let rawXml = '' ;
608+ const req = s3 . listObjectsV2 ( { Bucket : bucket } ) ;
609+
610+ req . on ( 'build' , ( ) => {
611+ req . httpRequest . headers [ 'x-amz-optional-object-attributes' ] = headerValue ;
612+ } ) ;
613+ req . on ( 'httpData' , chunk => { rawXml += chunk ; } ) ;
614+ req . on ( 'error' , err => reject ( err ) ) ;
615+ req . on ( 'success' , response => {
616+ parseString ( rawXml , ( err , parsedXml ) => {
617+ if ( err ) {
618+ return reject ( err ) ;
619+ }
620+
621+ const contents = response . data . Contents ;
622+ const parsedContents = parsedXml . ListBucketResult . Contents ;
623+
624+ if ( ! contents || ! parsedContents ) {
625+ return resolve ( response . data ) ;
626+ }
627+
628+ if ( parsedContents [ 0 ] ?. [ 'x-amz-meta-department' ] ) {
629+ contents [ 0 ] [ 'x-amz-meta-department' ] = parsedContents [ 0 ] [ 'x-amz-meta-department' ] [ 0 ] ;
630+ }
631+
632+ if ( parsedContents [ 0 ] ?. [ 'x-amz-meta-hr' ] ) {
633+ contents [ 0 ] [ 'x-amz-meta-hr' ] = parsedContents [ 0 ] [ 'x-amz-meta-hr' ] [ 0 ] ;
634+ }
635+
636+ return resolve ( response . data ) ;
637+ } ) ;
638+ } ) ;
639+
640+ req . send ( ) ;
641+ } ) ;
642+
643+ it ( 'should return an XML if the header is set' , async ( ) => {
644+ const s3 = bucketUtil . s3 ;
645+ const Bucket = bucketName ;
646+
647+ await s3 . putObject ( {
648+ Bucket,
649+ Key : 'super-power-object' ,
650+ Metadata : {
651+ Department : 'sales' ,
652+ HR : 'true' ,
653+ } ,
654+ } ) . promise ( ) ;
655+ const result = await listObjectsV2WithOptionalAttributes (
656+ s3 ,
657+ Bucket ,
658+ 'x-amz-meta-*,RestoreStatus,x-amz-meta-department' ,
659+ ) ;
660+
661+ assert . strictEqual ( result . Contents . length , 1 ) ;
662+ assert . strictEqual ( result . Contents [ 0 ] . Key , 'super-power-object' ) ;
663+ assert . strictEqual ( result . Contents [ 0 ] [ 'x-amz-meta-department' ] , 'sales' ) ;
664+ assert . strictEqual ( result . Contents [ 0 ] [ 'x-amz-meta-hr' ] , 'true' ) ;
665+ } ) ;
666+
667+ it ( 'should reject the request if the user does not have the permission' , async ( ) => {
668+ const s3 = bucketUtil . s3 ;
669+ const Bucket = bucketName ;
670+
671+ await s3 . putObject ( {
672+ Bucket,
673+ Key : 'super-power-object' ,
674+ Metadata : {
675+ Department : 'sales' ,
676+ HR : 'true' ,
677+ } ,
678+ } ) . promise ( ) ;
679+
680+ try {
681+ await listObjectsV2WithOptionalAttributes (
682+ s3ClientWithoutPermission ,
683+ Bucket ,
684+ 'x-amz-meta-*,RestoreStatus,x-amz-meta-department' ,
685+ ) ;
686+ throw new Error ( 'Request should have been rejected' ) ;
687+ } catch ( err ) {
688+ assert . strictEqual ( err . statusCode , 403 ) ;
689+ assert . strictEqual ( err . code , 'AccessDenied' ) ;
690+ }
691+ } ) ;
692+
693+ it ( 'should always (ignore permission) return an XML when the header is RestoreStatus' , async ( ) => {
694+ const s3 = bucketUtil . s3 ;
695+ const Bucket = bucketName ;
696+
697+ await s3 . putObject ( {
698+ Bucket,
699+ Key : 'super-power-object' ,
700+ Metadata : {
701+ Department : 'sales' ,
702+ HR : 'true' ,
703+ } ,
704+ } ) . promise ( ) ;
705+ const result = await listObjectsV2WithOptionalAttributes (
706+ s3ClientWithoutPermission ,
707+ Bucket ,
708+ 'RestoreStatus' ,
709+ ) ;
710+
711+ assert . strictEqual ( result . Contents . length , 1 ) ;
712+ assert . strictEqual ( result . Contents [ 0 ] . Key , 'super-power-object' ) ;
713+ assert . strictEqual ( result . Contents [ 0 ] [ 'x-amz-meta-department' ] , undefined ) ;
714+ assert . strictEqual ( result . Contents [ 0 ] [ 'x-amz-meta-hr' ] , undefined ) ;
715+ } ) ;
716+ } ) ;
538717 } ) ;
539718} ) ;
0 commit comments