Skip to content

Commit a5daaa8

Browse files
committed
public folders - wip
1 parent fc0f8e8 commit a5daaa8

File tree

19 files changed

+586
-85
lines changed

19 files changed

+586
-85
lines changed

Untitled-1.java

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
setting f1 to public - db:I, s3:Allow
2+
f2 shows as public - db:null, s3:null
3+
o1 shows as public - db:null, s3:null
4+
5+
6+
setting f1 to private - db:0, s3:null
7+
f2 shows as private - db:null, S3:null
8+
o1 is private - db:null, S3: null
9+
10+
// coms public = where object/bucket.public is true... or if null, where a parent is true.
11+
// coms not public = where object/bucket.public is false or if null, where a parent is false.
12+
13+
// let status;
14+
// if(resource.public) status = resource.public;
15+
// else{
16+
// // get parents
17+
// sort by key length
18+
// get status from first in array where not null
19+
// }
20+
21+
but the problem with this is that calculating public status is too costly
22+
23+
24+
coms public = object.public = true, null or false is false
25+

app/src/components/constants.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ module.exports = Object.freeze({
5757
/** Maximum Content Length supported by S3 CopyObjectCommand */
5858
MAXCOPYOBJECTLENGTH: 5 * 1024 * 1024 * 1024, // 5 GB
5959

60-
/** Maximum Content Length supported by S3 CopyObjectCommand */
60+
/** Maximum Content Length (file size) supported by S3 */
6161
MAXFILEOBJECTLENGTH: 5 * 1024 * 1024 * 1024 * 1024, // 5 TB
6262

6363
/** Allowable values for the Metadata Directive parameter */

app/src/components/queueManager.js

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class QueueManager {
5858
if (!this.isBusy && !this.toClose) {
5959
objectQueueService.queueSize().then(size => {
6060
if (size > 0) this.processNextJob();
61+
// if (size > 0);
6162
}).catch((err) => {
6263
log.error(`Error encountered while checking queue: ${err.message}`, { function: 'checkQueue', error: err });
6364
});

app/src/components/utils.js

+15
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,21 @@ const utils = {
357357
&& pathParts.filter(part => !prefixParts.includes(part)).length === 1;
358358
},
359359

360+
/**
361+
* @function isBelowPrefix
362+
* Predicate function determining if a path is 'below' a prefix
363+
* @param {string} prefix The base "folder"
364+
* @param {string} path The "sub-folder" to check
365+
* @returns {boolean} True if path is below of prefix. False in all other cases.
366+
*/
367+
isBelowPrefix(prefix, path) {
368+
if (typeof prefix !== 'string' || typeof path !== 'string') return false;
369+
else if (path === prefix) return false;
370+
else if (prefix === DELIMITER) return true;
371+
else if (path.startsWith(prefix)) return true;
372+
else return false;
373+
},
374+
360375
/**
361376
* @function isTruthy
362377
* Returns true if the element name in the object contains a truthy value

app/src/controllers/bucket.js

+43
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const log = require('../components/log')(module.filename);
99
const {
1010
addDashesToUuid,
1111
getCurrentIdentity,
12+
getBucket,
1213
isTruthy,
1314
joinPath,
1415
mixedQueryToArray,
@@ -323,6 +324,48 @@ const controller = {
323324
}
324325
},
325326

327+
328+
/**
329+
* @function togglePublic
330+
* Sets the public flag of a bucket (or folder)
331+
* @param {object} req Express request object
332+
* @param {object} res Express response object
333+
* @param {function} next The next callback function
334+
* @returns {function} Express middleware function
335+
*/
336+
async togglePublic(req, res, next) {
337+
try {
338+
const bucketId = addDashesToUuid(req.params.bucketId);
339+
const publicFlag = isTruthy(req.query.public) ?? false;
340+
const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER), SYSTEM_USER);
341+
342+
const bucket = await getBucket(bucketId);
343+
const data = {
344+
bucketId: bucketId,
345+
path: bucket.key + '/',
346+
public: publicFlag,
347+
userId: userId
348+
};
349+
await storageService.updatePublic(data).catch(() => {
350+
log.warn('Failed to apply permission changes to S3', { function: 'togglePublic', ...data });
351+
});
352+
// const s3Public = await storageService.getPublic({ path: data.path, bucketId: bucketId });
353+
// console.log('s3Public', s3Public);
354+
355+
const response = await bucketService.updatePublic({
356+
...bucket,
357+
bucketId: bucketId,
358+
public: publicFlag,
359+
userId: userId
360+
});
361+
362+
res.status(200).json(response);
363+
} catch (e) {
364+
next(errorToProblem(SERVICE, e));
365+
}
366+
},
367+
368+
326369
/**
327370
* @function updateBucket
328371
* Updates a bucket

app/src/controllers/object.js

+11-9
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,9 @@ const controller = {
308308
existingObjectId: objectId,
309309
});
310310

311-
} catch (err) {
311+
}
312+
// headObject threw an error because object was not found
313+
catch (err) {
312314
if (err instanceof Problem) throw err; // Rethrow Problem type errors
313315

314316
// Object is soft deleted from the bucket
@@ -1074,19 +1076,19 @@ const controller = {
10741076
const data = {
10751077
id: objId,
10761078
bucketId: req.currentObject?.bucketId,
1077-
filePath: req.currentObject?.path,
1079+
path: req.currentObject?.path,
10781080
public: publicFlag,
10791081
userId: userId,
1080-
// TODO: Implement if/when we proceed with version-scoped public permission management
1081-
// s3VersionId: await getS3VersionId(
1082-
// req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId
1083-
// )
10841082
};
10851083

1086-
storageService.putObjectPublic(data).catch(() => {
1087-
// Gracefully continue even when S3 ACL management operation fails
1088-
log.warn('Failed to apply ACL permission changes to S3', { function: 'togglePublic', ...data });
1084+
await storageService.updatePublic(data).catch(() => {
1085+
log.warn('Failed to apply permission changes to S3', { function: 'togglePublic', ...data });
10891086
});
1087+
1088+
const s3Public = await storageService.getPublic({ path: data.path, bucketId: req.currentObject?.bucketId });
1089+
1090+
console.log('s3Public', s3Public);
1091+
10901092
const response = await objectService.update(data);
10911093

10921094
res.status(200).json(response);

app/src/controllers/sync.js

+9
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ const controller = {
141141
dbBuckets = dbBuckets.filter(b => b.bucketId !== dbBucket.bucketId);
142142
})
143143
)
144+
// TODO: delete COMS S3 Policies for deleted COMS buckets and child objects.
144145
);
146+
145147
// add current user's permissions to all buckets
146148
await Promise.all(
147149
dbBuckets.map(bucket => {
@@ -176,6 +178,13 @@ const controller = {
176178
});
177179
})
178180
);
181+
182+
183+
// Sync Public Policies
184+
// S3 Bucket Policies applied by COMS to make a file or prefix 'public'
185+
// need to be synced with the bucket.public value in COMS db
186+
187+
179188
return dbBuckets;
180189
}
181190
catch (err) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
exports.up = function (knex) {
2+
return Promise.resolve()
3+
// allow null for object.public
4+
.then(() => knex.schema.alterTable('object', table => {
5+
table.boolean('public').nullable().alter();
6+
}))
7+
// where object.public is false, set to null
8+
.then(() => knex('object')
9+
.where({ 'public': false })
10+
.update({ 'public': null }))
11+
.then(() => knex.schema.alterTable('object', table => {
12+
table.boolean('public').nullable().alter();
13+
}))
14+
// add public column to bucket table
15+
.then(() => knex.schema.alterTable('bucket', table => {
16+
table.boolean('public');
17+
}));
18+
};
19+
20+
exports.down = function (knex) {
21+
return Promise.resolve()
22+
// drop public column in bucket table
23+
.then(() => knex.schema.alterTable('bucket', table => {
24+
table.dropColumn('public');
25+
}))
26+
// where object.public is null, set to false
27+
.then(() => knex('object')
28+
.where({ 'public': null })
29+
.update({ 'public': false }))
30+
31+
// disallow null for object.public
32+
.then(() => knex.schema.alterTable('object', table => {
33+
table.boolean('public').notNullable().alter();
34+
}));
35+
};

app/src/db/models/tables/bucket.js

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ class Bucket extends mixin(Model, [
9999
region: { type: 'string', maxLength: 255 },
100100
active: { type: 'boolean' },
101101
lastSyncRequestedDate: { type: ['string', 'null'], format: 'date-time' },
102+
public: { type: 'boolean' },
102103
...stamps
103104
},
104105
additionalProperties: false

app/src/middleware/authorization.js

+92-4
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ const _checkPermission = async ({ currentObject, currentUser, params }, permissi
3535
const searchParams = { permCode: permission, userId: userId };
3636

3737
if (params.objectId) {
38+
// add object permissions
3839
permissions.push(...await objectPermissionService.searchPermissions({
3940
objId: params.objectId, ...searchParams
4041
}));
4142
}
4243
if (params.bucketId || currentObject.bucketId) {
44+
// add bucket permissions
4345
permissions.push(...await bucketPermissionService.searchPermissions({
4446
bucketId: params.bucketId || currentObject.bucketId, ...searchParams
4547
}));
@@ -52,6 +54,7 @@ const _checkPermission = async ({ currentObject, currentUser, params }, permissi
5254
log.debug('Missing user identification', { function: '_checkPermission' });
5355
return result;
5456
};
57+
5558
/**
5659
* @function checkS3BasicAccess
5760
* Checks and authorized access to perform operation for s3 basic authentication request
@@ -138,6 +141,31 @@ const checkAppMode = (req, _res, next) => {
138141
next();
139142
};
140143

144+
/**
145+
* @function currentBucket
146+
* Injects a currentBucket object to the request if there is an applicable bucket record
147+
* @param {object} req Express request object
148+
* @param {object} _res Express response object
149+
* @param {function} next The next callback function
150+
* @returns {function} Express middleware function
151+
*/
152+
// const currentBucket = async (req, _res, next) => {
153+
// try {
154+
// if (mixedQueryToArray(req.query.bucketId).length === 1) {
155+
// const bucket = await bucketService.read(req.query.bucketId);
156+
// if (bucket) {
157+
// req.currentBucket = Object.freeze({
158+
// ...redactSecrets(bucket, ['accessKeyId', 'secretAccessKey'])
159+
// });
160+
// }
161+
// }
162+
// } catch (err) {
163+
// log.warn(err.message, { function: 'currentBucket' });
164+
// }
165+
// next();
166+
// };
167+
168+
141169
/**
142170
* @function currentObject
143171
* Injects a currentObject object to the request if there is an applicable object record
@@ -190,15 +218,22 @@ const hasPermission = (permission) => {
190218
throw new Error('Missing object record');
191219
} else if (authType === AuthType.BASIC && canBasicMode(authMode)) {
192220
log.debug('Basic authTypes are always permitted', { function: 'hasPermission' });
193-
} else if (req.params.objectId && req.currentObject.public && permission === Permissions.READ) {
221+
}
222+
// if reading a public object
223+
else if (req.params.objectId && await isObjectPublic(req.currentObject) && permission === Permissions.READ) {
194224
log.debug('Read requests on public objects are always permitted', { function: 'hasPermission' });
195-
} else if (!await _checkPermission(req, permission)) {
225+
}
226+
// if reading a public bucket
227+
else if (req.params.bucketId && await isBucketPublic(req.params.bucketId) && permission === Permissions.READ) {
228+
log.debug('Read requests on public buckets are always permitted', { function: 'hasPermission' });
229+
}
230+
else if (!await _checkPermission(req, permission)) {
196231
throw new Error(`User lacks required permission ${permission}`);
197232
}
198233
} catch (err) {
199234
log.verbose(err.message, { function: 'hasPermission' });
200235
return next(new Problem(403, {
201-
detail: 'User lacks permission to complete this action',
236+
detail: 'User lacks permission to complete this action' + err,
202237
instance: req.originalUrl
203238
}));
204239
}
@@ -207,6 +242,59 @@ const hasPermission = (permission) => {
207242
};
208243
};
209244

245+
// const isBucketPublic = async (req, _res, next) => {
246+
// // if an unauthenticated request
247+
// if (!req.currentUser || req.currentUser.authType === AuthType.NONE) {
248+
// // if providing a single bucketId in query
249+
// if (mixedQueryToArray(req.query.bucketId).length === 1) {
250+
// const bucket = await bucketService.read(req.query.bucketId);
251+
// // and bucket public is truthy
252+
// if (!bucket.public) {
253+
// return next(new Problem(403, {
254+
// detail: 'Bucket is not public',
255+
// instance: req.originalUrl
256+
// }));
257+
// }
258+
// }
259+
// }
260+
// else {
261+
// return next(new Problem(403, {
262+
// detail: 'User lacks permission to complete this action',
263+
// instance: req.originalUrl
264+
// }));
265+
// }
266+
// next();
267+
// };
268+
269+
270+
/**
271+
* get public status from COMS database
272+
* checks current object and all parent folders
273+
*/
274+
const isObjectPublic = async (currentObject) => {
275+
if (currentObject.public) return true;
276+
if (await isBucketPublic(currentObject.bucketId)) return true;
277+
return false;
278+
};
279+
280+
/**
281+
* get public status from COMS database
282+
* checks current folder and all parent folders
283+
*/
284+
const isBucketPublic = async (bucketId) => {
285+
const bucket = await bucketService.read(bucketId);
286+
if (bucket.public) return true;
287+
const parentBuckets = await bucketService.searchParentBuckets(bucket);
288+
if (parentBuckets.some(b => b.public)) return true;
289+
return false;
290+
};
291+
210292
module.exports = {
211-
_checkPermission, checkAppMode, checkS3BasicAccess, currentObject, hasPermission
293+
_checkPermission,
294+
checkAppMode,
295+
isBucketPublic,
296+
isObjectPublic,
297+
checkS3BasicAccess,
298+
currentObject,
299+
hasPermission
212300
};

0 commit comments

Comments
 (0)