11const {
22 CopyObjectCommand,
3+ DeleteBucketPolicyCommand,
34 DeleteObjectCommand,
45 DeleteObjectTaggingCommand,
56 GetBucketEncryptionCommand,
@@ -14,7 +15,6 @@ const {
1415 PutObjectAclCommand,
1516 GetBucketPolicyCommand,
1617 PutBucketPolicyCommand,
17- DeleteBucketPolicyCommand,
1818 PutObjectCommand,
1919 PutBucketEncryptionCommand,
2020 PutObjectTaggingCommand,
@@ -360,7 +360,7 @@ const objectStorageService = {
360360 Bucket : data . bucket ,
361361 ContinuationToken : continuationToken ,
362362 MaxKeys : maxKeys ,
363- Prefix : filePath ?? prefix // Must filter via " prefix" - https://stackoverflow.com/a/56569856
363+ Prefix : filePath ?? prefix // Must filter via ' prefix' - https://stackoverflow.com/a/56569856
364364 } ;
365365
366366 return this . _getS3Client ( data ) . send ( new ListObjectsV2Command ( params ) ) ;
@@ -384,7 +384,7 @@ const objectStorageService = {
384384 Bucket : data . bucket ,
385385 KeyMarker : keyMarker ,
386386 MaxKeys : maxKeys ,
387- Prefix : filePath ?? prefix // Must filter via " prefix" - https://stackoverflow.com/a/56569856
387+ Prefix : filePath ?? prefix // Must filter via ' prefix' - https://stackoverflow.com/a/56569856
388388 } ;
389389
390390 return this . _getS3Client ( data ) . send ( new ListObjectVersionsCommand ( params ) ) ;
@@ -476,105 +476,127 @@ const objectStorageService = {
476476
477477 /**
478478 * removes public ACL, where ACL grants ALLUSERS READ permission,
479- * for an object
480- * will also remove this type of ACL set by other applications
479+ * for all objects at or below a given path
480+ * note: will also remove this type of ACL set by other applications
481481 */
482- async removePublicACL ( data , key ) {
483- const existingAcl = await this . _getS3Client ( data ) . send ( new GetObjectAclCommand ( {
484- Bucket : data . bucket ,
485- Key : key
486- } ) ) ;
487- const existingGrants = existingAcl . Grants ;
488- const hasPublicAcl = existingGrants . some ( grant => grant . Grantee ?. URI === ALLUSERS && grant . Permission === 'READ' ) ;
489- if ( hasPublicAcl ) {
490- await this . _getS3Client ( data ) . send ( new PutObjectAclCommand ( {
491- AccessControlPolicy : {
492- Grants : existingGrants
493- . filter ( grant => grant . Grantee ?. URI !== ALLUSERS && grant . Permission !== 'READ' )
494- . map ( grant => {
495- return {
496- Grantee : grant . Grantee ,
497- Permission : grant . Permission ,
498- } ;
499- } ) ,
500- Owner : existingAcl . Owner
501- } ,
502- Bucket : data . bucket ,
503- Key : key ,
504- } ) ) ;
505- return true ;
482+ async removePublicACL ( bucketId , path , recursive = false ) {
483+ try {
484+ const data = await utils . getBucket ( bucketId ) ;
485+ const key = utils . stripDelimit ( path ) ;
486+ let objects = [ ] ;
487+
488+ // list all objects at and below provided path (key)
489+ if ( recursive ) {
490+ const s3Response = await this . listAllObjectVersions ( { filePath : key , bucketId : bucketId , precisePath : false } ) ;
491+ log . info ( `Found ${ s3Response . Versions . length } object versions and ${ s3Response . DeleteMarkers . length }
492+ delete markers in S3 for bucketId ${ data . bucketId } ` , { function : 'syncBucketRecursive' } ) ;
493+ const s3Keys = [ ...new Set ( [
494+ ...s3Response . DeleteMarkers . map ( object => object . Key ) ,
495+ ...s3Response . Versions . map ( object => object . Key ) ,
496+ ] ) ] ;
497+ objects = s3Keys ;
498+ } else {
499+ objects = [ key ] ;
500+ }
501+ log . info ( `Removing ACL's from ${ objects . length } objects` , { function : 'removePublicACL' } ) ;
502+
503+ // for each object update existing ACL to remove READ on ALLUSERS
504+ for ( const objKey of objects . slice ( 0 , 1000 ) ) { // limit to 1000 to avoid timeouts
505+ const existingAcl = await this . _getS3Client ( data ) . send ( new GetObjectAclCommand ( {
506+ Bucket : data . bucket ,
507+ Key : objKey
508+ } ) ) ;
509+ const existingGrants = existingAcl . Grants ;
510+ const hasPublicAcl = existingGrants . some (
511+ grant => grant . Grantee ?. URI === ALLUSERS && grant . Permission === 'READ'
512+ ) ;
513+ if ( hasPublicAcl ) {
514+ await this . _getS3Client ( data ) . send ( new PutObjectAclCommand ( {
515+ AccessControlPolicy : {
516+ Grants : existingGrants
517+ . filter ( grant => grant . Grantee ?. URI !== ALLUSERS && grant . Permission !== 'READ' )
518+ . map ( grant => {
519+ return {
520+ Grantee : grant . Grantee ,
521+ Permission : grant . Permission ,
522+ } ;
523+ } ) ,
524+ Owner : existingAcl . Owner
525+ } ,
526+ Bucket : data . bucket ,
527+ Key : objKey ,
528+ } ) ) ;
529+ }
530+ }
531+ } catch ( error ) {
532+ log . error ( 'Error in removePublicACL:' , error ) ;
533+ throw error ;
506534 }
507535 } ,
508536
509537 /**
510538 * @function updatePublic
511539 * Updates the S3 Bucket Policies for the given resource
540+ * any non-COMS Bucket Policies (Sid starts with `coms::`) are preserved
541+ * NOTE: This function will remove legacy ACL's for all resources at or below path when setting to private
542+ * to ensure objects are not inadvertently left public due to ACL settings
512543 * @param {string } path the path of the resource
513544 * @param {boolean } whether given resource should be set to public
514545 * @param {string } bucketId of COMS bucket for the resource
515546 * @returns {Promise<object> } The S3 PutBucketPolicyCommand response
516547 */
517- async updatePublic ( { path, public : publicFlag = false , bucketId } ) {
548+ async updatePublic ( { path, public : setAsPublic = false , bucketId } ) {
518549 const data = await utils . getBucket ( bucketId ) ;
519550 const isPrefix = data . key + '/' === path ;
520551 const resource = data . bucket + '/' + path ;
521- let existingStatement = [ ] ;
552+ let existingPolicies = [ ] ;
522553 let s3Response = [ ] ;
523554
524555 // Get existing policy
525556 try {
526557 const existingPolicy = await this . _getS3Client ( data ) . send ( new GetBucketPolicyCommand ( { Bucket : data . bucket } ) ) ;
527- existingStatement = JSON . parse ( existingPolicy . Policy ) . Statement ;
558+ existingPolicies = JSON . parse ( existingPolicy . Policy ) . Statement ;
559+ // If COMS policy found for any parent prefix, throw error, exceptions from inherited policies are not permitted
560+ const parentPolicy = existingPolicies . find ( policy =>
561+ `coms::${ resource } ` . startsWith ( policy . Sid ) &&
562+ `coms::${ resource } ` !== policy . Sid ) ;
563+ if ( parentPolicy ) {
564+ throw new Error ( `Unable to override Public status set on folder: ${ parentPolicy . Resource } ` ) ;
565+ }
528566 } catch ( e ) {
529567 log . debug ( 'No existing policy found' , { function : 'updatePublic' } ) ;
530568 }
531569
532- // If COMS policy found for any parent prefix, throw error. no exceptions to inherited policies permitted
533- const parentPolicy = existingStatement . find ( policy =>
534- `coms::${ resource } ` . startsWith ( policy . Sid ) && `coms::${ resource } ` !== policy . Sid ) ;
535- if ( parentPolicy ) {
536- throw new Error ( `Unable to override Public status set on folder: ${ parentPolicy . Resource } ` ) ;
537- }
538-
539570 // --- Update policy
540- // if making public add Allow rule for this resource
541- if ( publicFlag ) {
542-
543- // include any existing non-coms-managed rules for this resource or below
544- const newStatement = existingStatement
545- . filter ( policy => ! policy . Sid . startsWith ( `coms::${ resource } ` ) ) ;
546-
571+ // keep non-coms and sibling policies
572+ let newPolicies = existingPolicies . filter ( policy => {
573+ return ! policy . Sid . startsWith ( utils . trimResourcePath ( `coms::${ resource } ` ) ) ;
574+ } ) ;
575+ // if making resource private and no other policies remain, remove entire policy
576+ if ( newPolicies . length === 0 && ! setAsPublic ) {
577+ await this . _getS3Client ( data ) . send ( new DeleteBucketPolicyCommand ( { Bucket : data . bucket } ) ) ;
578+ }
579+ // if making public add Allow rule for this resource (and below)
580+ if ( setAsPublic ) {
547581 const resourceKey = isPrefix ? resource + '*' : resource ; // prefixes/need/trailing/wildcard/*
548- newStatement
582+ newPolicies
549583 . push ( {
550584 Action : 's3:GetObject' ,
551585 Resource : resourceKey ,
552586 Effect : 'Allow' ,
553587 Principal : '*' ,
554588 Sid : 'coms::' + resource ,
555589 } ) ;
556-
557- // put new policy
558- if ( newStatement . length > 0 ) {
559- s3Response = await this . _getS3Client ( data ) . send ( new PutBucketPolicyCommand ( {
560- Bucket : data . bucket ,
561- Policy : JSON . stringify ( { Version : '2012-10-17' , Statement : newStatement } )
562- } ) ) ;
563- }
564- }
565-
566- // else setting to private
567- // note: logic is confusing. But here, if setting to private, we are only deleting a COMS policy
568- else if ( existingStatement . length > 0 ) {
569- await this . _getS3Client ( data ) . send ( new DeleteBucketPolicyCommand ( { Bucket : data . bucket } ) ) ;
570- s3Response = [ ] ;
571590 }
572-
573- // If updating a single file,
574- // Remove public ACL for file if present (phase out previous implementation)
575- if ( isPrefix === false ) {
576- await this . removePublicACL ( data , path ) ;
591+ if ( newPolicies . length > 0 ) {
592+ s3Response = await this . _getS3Client ( data ) . send ( new PutBucketPolicyCommand ( {
593+ Bucket : data . bucket ,
594+ Policy : JSON . stringify ( { Version : '2012-10-17' , Statement : newPolicies } )
595+ } ) ) ;
577596 }
597+ // -- remove all ACL's for this resource and below as precaution to user
598+ // if updating a single object or setting a folder private,
599+ if ( ! isPrefix || ! setAsPublic ) await this . removePublicACL ( bucketId , path , isPrefix ) ;
578600
579601 return s3Response ;
580602 } ,
@@ -589,10 +611,10 @@ const objectStorageService = {
589611 async getPublic ( { path, bucketId } ) {
590612 const data = await utils . getBucket ( bucketId ) ;
591613 const resource = data . bucket + '/' + path ;
592- // if resource is an object, check for public ACL's
614+ const hasPublicPolicy = await this . hasEffectivePublicPolicy ( resource , data ) ;
615+ // if resource is an object, check for public ACL's (ACL's cannot apply to prefixes)
593616 const hasPublicAcl = data . key !== resource ? await this . hasPublicAcl ( data , path ) : false ;
594617 // Check for COMS Bucket Policy for this resource
595- const hasPublicPolicy = await this . hasEffectivePublicPolicy ( resource , data ) ;
596618 return hasPublicAcl || hasPublicPolicy ;
597619 } ,
598620
@@ -606,20 +628,29 @@ const objectStorageService = {
606628 try {
607629 const existingPolicy = await this . _getS3Client ( data ) . send ( new GetBucketPolicyCommand ( { Bucket : data . bucket } ) ) ;
608630 const statement = JSON . parse ( existingPolicy . Policy ) . Statement ;
609- // order policies by string length to get most specific
610- const sortedpolicies = statement . sort ( ( a , b ) => b . Resource . length - a . Resource . length ) ;
611- let policy ;
612- for ( let i = 0 ; i < sortedpolicies . length ; i ++ ) {
613- policy = sortedpolicies [ i ] ;
614- if ( resource . startsWith ( policy . Resource || `${ policy . Resource } *` ) &&
615- policy . Sid . startsWith ( 'coms::' ) &&
616- policy . Principal === '*' ) {
617- break ;
618- }
631+ // A Deny policy on resource or above, which override Allow policies will set public status to false
632+ const denyPolicies = statement
633+ . filter ( p => {
634+ return p . Effect === 'Deny' &&
635+ resource . startsWith ( utils . trimResourcePath ( p . Resource ) ) &&
636+ p . Principal === '*' ;
637+ } ) ;
638+ if ( denyPolicies . length > 0 ) {
639+ return false ;
640+ }
641+ // Any Allow policy on resource or above will set public status to true
642+ else {
643+ const allowPolicies = statement
644+ . filter ( p => {
645+ return p . Effect === 'Allow' &&
646+ resource . startsWith ( utils . trimResourcePath ( p . Resource ) ) &&
647+ p . Principal === '*' ;
648+ } )
649+ . sort ( ( a , b ) => b . Resource . length - a . Resource . length ) ;
650+ return ( allowPolicies . length > 0 ) ? true : false ;
619651 }
620- return policy . Effect === 'Allow' ? true : false ;
621652 } catch ( e ) {
622- log . debug ( 'No existing policies found' , { function : 'getPublic' } ) ;
653+ log . info ( 'No existing effective policies found' , { function : 'getPublic' } ) ;
623654 return false ;
624655 }
625656 } ,
0 commit comments