Skip to content

Commit d30ea1a

Browse files
committed
Use plain S3 url to get public objects
Add `GetObjectVersion` permission to `public` bucket policies Handle sql transaction during syncBucketRecords
1 parent 166dd4e commit d30ea1a

File tree

4 files changed

+77
-31
lines changed

4 files changed

+77
-31
lines changed

app/src/components/utils.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,28 @@ const utils = {
275275
return array.find(obj => (obj.key === key && obj.value === value));
276276
},
277277

278+
279+
/**
280+
* @function getS3Url
281+
* Constructs the S3 URL for a given file in a specified bucket.
282+
* @param {object} data An object containing the necessary information to construct the S3 URL.
283+
* @param {string} data.bucketId The ID of the S3 bucket.
284+
* @param {string} data.filePath The path of the file within the bucket.
285+
* @param {string} [data.s3VersionId] An optional S3 version ID for the file.
286+
* @returns {Promise<string>} A promise that resolves to the constructed S3 URL.
287+
* @throws {Error} If there is an issue retrieving the bucket details.
288+
*/
289+
async getS3Url(data) {
290+
// get bucket details
291+
const { read } = require('../services/bucket');
292+
const bucket = await read(data.bucketId);
293+
let url = `${bucket.endpoint}/${bucket.bucket}/${data.filePath.replace(/^\/|\/$/g, '')}`;
294+
if (data.s3VersionId) {
295+
url += `?versionId=${data.s3VersionId}`;
296+
}
297+
return url;
298+
},
299+
278300
/**
279301
* @function getS3VersionId
280302
* Gets the s3VersionId from database using given internal COMS version id

app/src/controllers/object.js

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const {
2121
getCurrentIdentity,
2222
getKeyValue,
2323
getMetadata,
24+
getS3Url,
2425
getS3VersionId,
2526
joinPath,
2627
isTruthy,
@@ -879,18 +880,27 @@ const controller = {
879880
response.Body.pipe(res); // Stream body content directly to response
880881
res.status(200);
881882
}
882-
} else {
883-
const signedUrl = await storageService.readSignedUrl({
884-
expiresIn: req.query.expiresIn,
885-
...data
886-
});
883+
}
884+
else {
885+
let s3Url;
886+
// if object is public, construct S3 url manually
887+
if (req.currentObject.public) {
888+
s3Url = await getS3Url(data);
889+
}
890+
// else get pre-signed S3 url
891+
else {
892+
s3Url = await storageService.readSignedUrl({
893+
expiresIn: req.query.expiresIn,
894+
...data
895+
});
896+
}
887897

888-
// Present download url link
898+
// if request was for a url, present download url link
889899
if (req.query.download && req.query.download === DownloadMode.URL) {
890-
res.status(201).json(signedUrl);
900+
res.status(201).json(s3Url);
891901
// Download via HTTP redirect
892902
} else {
893-
res.status(302).set('Location', signedUrl).end();
903+
res.status(302).set('Location', s3Url).end();
894904
}
895905
}
896906

app/src/controllers/sync.js

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,15 @@ const controller = {
113113
const bucket = await bucketService.read(bucketId);
114114
const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER), SYSTEM_USER);
115115

116-
// sync bucket.public flag
117-
await this.syncBucketPublic(bucket.key, bucket.bucketId, userId);
118116

119117
const s3Objects = await storageService.listAllObjectVersions({ bucketId: bucketId, filterLatest: true });
120118
log.info(`Found ${s3Objects.Versions.length} object versions and ${s3Objects.DeleteMarkers.length}
121119
delete markers in S3 for bucketId ${bucketId}`, { function: 'syncBucketSingle' });
122120

123121
const response = await utils.trxWrapper(async (trx) => {
122+
// sync bucket.public flag
123+
await this.syncBucketPublic(bucket, userId, trx);
124+
// queue objects
124125
return this.queueObjectRecords([bucket], s3Objects, userId, trx);
125126
});
126127

@@ -207,7 +208,7 @@ const controller = {
207208
trx
208209
);
209210
// --- Sync S3 Bucket Policies applied by COMS
210-
await this.syncBucketPublic(bucket.key, bucket.bucketId, userId);
211+
await this.syncBucketPublic(bucket, userId, trx);
211212
})
212213
);
213214
return dbBuckets;
@@ -218,16 +219,27 @@ const controller = {
218219
}
219220
},
220221

221-
async syncBucketPublic(key, bucketId, userId) {
222+
/**
223+
* Synchronizes the public status of a bucket with the storage service.
224+
* @async
225+
* @function syncBucketPublic
226+
* @param {Object} bucket - The bucket object to synchronize
227+
* @param {string} userId - The ID of the user performing the sync operation
228+
* @param {Object} trx - The database transaction object
229+
* @returns {Promise<void>}
230+
* @description Fetches the public status from the storage service for the given bucket
231+
* and updates the bucket record with the retrieved public status and the user who performed the update.
232+
*/
233+
async syncBucketPublic(bucket, userId, trx) {
222234
let isPublic = false;
223-
isPublic = await storageService.getPublic({ path: key, bucketId: bucketId });
224-
bucketService.update({
225-
bucketId: bucketId,
235+
isPublic = await storageService.getPublic({ path: bucket.key, bucket: bucket }, trx);
236+
await bucketService.update({
237+
bucketId: bucket.bucketId,
226238
updatedBy: userId,
227239
public: isPublic
228240
// TODO: consider changing this to actual lastSyncDate
229241
// lastSyncRequestedDate: now(),
230-
});
242+
}, trx);
231243
},
232244

233245
/**

app/src/services/storage.js

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const { Upload } = require('@aws-sdk/lib-storage');
2424
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
2525
const config = require('config');
2626

27-
const { ALLUSERS, MetadataDirective, TaggingDirective } = require('../components/constants');
27+
const { ALLUSERS, DEFAULTREGION, MetadataDirective, TaggingDirective } = require('../components/constants');
2828
const log = require('../components/log')(module.filename);
2929
const utils = require('../components/utils');
3030

@@ -582,7 +582,7 @@ const objectStorageService = {
582582
const resourceKey = isPrefix ? resource + '*' : resource; // prefixes/need/trailing/wildcard/*
583583
newPolicies
584584
.push({
585-
Action: 's3:GetObject',
585+
Action: ['s3:GetObject', 's3:GetObjectVersion'],
586586
Resource: resourceKey,
587587
Effect: 'Allow',
588588
Principal: '*',
@@ -604,17 +604,18 @@ const objectStorageService = {
604604

605605
/**
606606
* @function getPublic
607-
* checks for a Bucket Policy or ACL that will make the given resource public
608-
* @param {string} path the path of the resource
609-
* @param {string} bucketId of COMS bucket for the resource
610-
* @returns {Promise<boolean>} whether the given resource is public
607+
* Checks for a Bucket Policy or ACL that will make the given resource public
608+
* @param {string} options.path The path of the resource to check
609+
* @param {string} [options.bucketId] Optional bucketId to retrieve bucket configuration
610+
* @param {object} [options.bucket] Optional bucket object containing bucketId (alternative to bucketId)
611+
* @returns {Promise<boolean>} True if the resource is public via policy or ACL, false otherwise
611612
*/
612-
async getPublic({ path, bucketId }) {
613-
const data = await utils.getBucket(bucketId);
614-
const resource = data.bucket + '/' + path;
615-
const hasPublicPolicy = await this.hasEffectivePublicPolicy(resource, data);
613+
async getPublic({ path, bucketId = undefined, bucket = undefined }) {
614+
const bucketData = bucket ? { ...bucket, region: DEFAULTREGION } : await utils.getBucket(bucketId);
615+
const resource = bucketData.bucket + '/' + path;
616+
const hasPublicPolicy = await this.hasEffectivePublicPolicy(resource, bucketData);
616617
// if resource is an object, check for public ACL's (ACL's cannot apply to prefixes)
617-
const hasPublicAcl = data.key !== resource ? await this.hasPublicAcl(data, path) : false;
618+
const hasPublicAcl = bucketData.key !== resource ? await this.hasPublicAcl(bucketData, path) : false;
618619
// Check for COMS Bucket Policy for this resource
619620
return hasPublicAcl || hasPublicPolicy;
620621
},
@@ -623,11 +624,12 @@ const objectStorageService = {
623624
* @function hasEffectivePublicPolicy
624625
* check for a Bucket Policy that will make the given resource public
625626
* @param {*} resource
626-
* @param {*} data
627+
* @param {*} bucketData
627628
*/
628-
async hasEffectivePublicPolicy(resource, data) {
629+
async hasEffectivePublicPolicy(resource, bucketData) {
629630
try {
630-
const existingPolicy = await this._getS3Client(data).send(new GetBucketPolicyCommand({ Bucket: data.bucket }));
631+
const existingPolicy = await this._getS3Client(bucketData)
632+
.send(new GetBucketPolicyCommand({ Bucket: bucketData.bucket }));
631633
const statement = JSON.parse(existingPolicy.Policy).Statement;
632634
// A Deny policy on resource or above, which override Allow policies will set public status to false
633635
const denyPolicies = statement
@@ -651,7 +653,7 @@ const objectStorageService = {
651653
return (allowPolicies.length > 0) ? true : false;
652654
}
653655
} catch (e) {
654-
log.debug('No existing effective policies found', { function: 'getPublic' });
656+
log.debug('No existing effective policies found', { function: 'hasEffectivePublicPolicy' });
655657
return false;
656658
}
657659
},

0 commit comments

Comments
 (0)