Skip to content

Commit d3d53d3

Browse files
committed
Remove legacy public ACL's when setting objects private
1 parent 13ccaa1 commit d3d53d3

File tree

4 files changed

+102
-62
lines changed

4 files changed

+102
-62
lines changed

app/src/components/utils.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,23 @@ const utils = {
505505
else return '';
506506
},
507507

508+
/**
509+
* @function stripResourcePath
510+
* Yields a string `s` without trailing delimiters or wildcards. Returns an empty string if falsy.
511+
* @param {string} s The input string
512+
* @returns {string} The string `s` without trailing delimiters or wildcards, or an empty string.
513+
*/
514+
stripResourcePath(s) {
515+
switch (true) {
516+
case s.endsWith(DELIMITER):
517+
return utils.stripDelimit(s.slice(0, -1));
518+
case s.endsWith(DELIMITER + '*'):
519+
return utils.stripDelimit(s.slice(0, -2));
520+
default:
521+
return s;
522+
}
523+
},
524+
508525
/**
509526
* @function toLowerKeys
510527
* Converts all key names for all objects in an array to lowercase

app/src/controllers/bucket.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ const controller = {
361361
};
362362
// Update S3 Policy
363363
await storageService.updatePublic(data).catch((e) => {
364-
log.warn('Failed to apply permission changes to S3' + e, { function: 'togglePublic', ...data });
364+
log.warn('Failed to apply permission changes to S3 ' + e, { function: 'togglePublic', ...data });
365365
});
366366

367367
// Child bucket cannot be non-public when parent is public

app/src/controllers/sync.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -221,13 +221,15 @@ const controller = {
221221
async syncBucketPublic(key, bucketId, userId) {
222222
let isPublic = false;
223223
isPublic = await storageService.getPublic({ path: key, bucketId: bucketId });
224-
bucketService.update({
225-
bucketId: bucketId,
226-
updatedBy: userId,
227-
public: isPublic
228-
// TODO: consider changing this to actual lastSyncDate
229-
// lastSyncRequestedDate: now(),
230-
});
224+
// TODO: make sure this works correctly
225+
console.log(`Syncing bucketId ${bucketId} public status to ${isPublic}`);
226+
// bucketService.update({
227+
// bucketId: bucketId,
228+
// updatedBy: userId,
229+
// public: isPublic
230+
// // TODO: consider changing this to actual lastSyncDate
231+
// // lastSyncRequestedDate: now(),
232+
// });
231233
},
232234

233235
/**

app/src/services/storage.js

Lines changed: 75 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ const {
1414
PutObjectAclCommand,
1515
GetBucketPolicyCommand,
1616
PutBucketPolicyCommand,
17-
DeleteBucketPolicyCommand,
1817
PutObjectCommand,
1918
PutBucketEncryptionCommand,
2019
PutObjectTaggingCommand,
@@ -479,42 +478,74 @@ const objectStorageService = {
479478
* for an object
480479
* will also remove this type of ACL set by other applications
481480
*/
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;
481+
async removePublicACL(bucketId, path, recursive = false) {
482+
try {
483+
const data = await utils.getBucket(bucketId);
484+
const key = utils.stripDelimit(path);
485+
let objects = [];
486+
487+
// list all objects at and below provided path (key)
488+
if (recursive) {
489+
const s3Response = await this.listAllObjectVersions({ filePath: key, bucketId: bucketId, precisePath: false });
490+
`Found ${s3Response.Versions.length} object versions and ${s3Response.DeleteMarkers.length}
491+
delete markers in S3 for bucketId ${data.bucketId}`, { function: 'syncBucketRecursive' });
492+
const s3Keys = [...new Set([
493+
...s3Response.DeleteMarkers.map(object => object.Key),
494+
...s3Response.Versions.map(object => object.Key),
495+
])];
496+
objects = s3Keys
497+
} else {
498+
objects = [key];
499+
}
500+
log.info(`Removing ACL's from ${objects.length} objects`, { function: 'removePublicACL' });
501+
502+
// for each object update existing ACL to remove READ on ALLUSERS
503+
for (const objKey of objects) {
504+
const existingAcl = await this._getS3Client(data).send(new GetObjectAclCommand({
505+
Bucket: data.bucket,
506+
Key: objKey
507+
}));
508+
const existingGrants = existingAcl.Grants;
509+
const hasPublicAcl = existingGrants.some(
510+
grant => grant.Grantee?.URI === ALLUSERS && grant.Permission === 'READ'
511+
);
512+
if (hasPublicAcl) {
513+
await this._getS3Client(data).send(new PutObjectAclCommand({
514+
AccessControlPolicy: {
515+
Grants: existingGrants
516+
.filter(grant => grant.Grantee?.URI !== ALLUSERS && grant.Permission !== 'READ')
517+
.map(grant => {
518+
return {
519+
Grantee: grant.Grantee,
520+
Permission: grant.Permission,
521+
};
522+
}),
523+
Owner: existingAcl.Owner
524+
},
525+
Bucket: data.bucket,
526+
Key: objKey,
527+
}));
528+
return true;
529+
}
530+
}
531+
} catch (error) {
532+
console.log('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;
@@ -529,21 +560,22 @@ const objectStorageService = {
529560
log.debug('No existing policy found', { function: 'updatePublic' });
530561
}
531562

532-
// If COMS policy found for any parent prefix, throw error. no exceptions to inherited policies permitted
563+
// If COMS policy found for any parent prefix, throw error, exceptions from inherited policies are not permitted
533564
const parentPolicy = existingStatement.find(policy =>
534-
`coms::${resource}`.startsWith(policy.Sid) && `coms::${resource}` !== policy.Sid);
565+
`coms::${resource}`.startsWith(policy.Sid) &&
566+
`coms::${resource}` !== policy.Sid);
535567
if (parentPolicy) {
568+
console.log('Parent policy found, cannot override public status set on parent folder');
536569
throw new Error(`Unable to override Public status set on folder: ${parentPolicy.Resource}`);
537570
}
538571

539572
// --- 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-
573+
// keep non-coms and sibling policies
574+
let newStatement = existingStatement.filter(policy => {
575+
return !policy.Sid.startsWith(utils.stripResourcePath(`coms::${resource}`));
576+
});
577+
// if making public add Allow rule for this resource (and below)
578+
if (setAsPublic) {
547579
const resourceKey = isPrefix ? resource + '*' : resource; // prefixes/need/trailing/wildcard/*
548580
newStatement
549581
.push({
@@ -553,28 +585,17 @@ const objectStorageService = {
553585
Principal: '*',
554586
Sid: 'coms::' + resource,
555587
});
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-
}
564588
}
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 = [];
589+
if (newStatement.length > 0) {
590+
s3Response = await this._getS3Client(data).send(new PutBucketPolicyCommand({
591+
Bucket: data.bucket,
592+
Policy: JSON.stringify({ Version: '2012-10-17', Statement: newStatement })
593+
}));
571594
}
572595

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);
577-
}
596+
// -- remove all ACL's for this resource and below as precaution to user
597+
// if updating a single object or setting a folder private,
598+
if (!isPrefix || !setAsPublic) await this.removePublicACL(bucketId, path, isPrefix);
578599

579600
return s3Response;
580601
},

0 commit comments

Comments
 (0)