diff --git a/app/src/components/log.js b/app/src/components/log.js index 3cdb79ff..e9ceb5d5 100644 --- a/app/src/components/log.js +++ b/app/src/components/log.js @@ -58,7 +58,8 @@ if (config.has('server.logFile')) { /** * Returns a Winston Logger or Child Winston Logger * @param {string} [filename] Optional module filename path to annotate logs with - * @returns {object} A child logger with appropriate metadata if `filename` is defined. Otherwise returns a standard logger. + * @returns {object} A child logger with appropriate metadata if `filename` is defined. + * Otherwise returns a standard logger. */ const getLogger = (filename) => { return filename ? log.child({ component: parse(filename).name }) : log; diff --git a/app/src/components/queueManager.js b/app/src/components/queueManager.js index b8585bbe..5d1975ab 100644 --- a/app/src/components/queueManager.js +++ b/app/src/components/queueManager.js @@ -97,18 +97,30 @@ class QueueManager { const objectId = await syncService.syncJob(job.path, job.bucketId, job.full, job.createdBy); - log.verbose(`Finished processing job id ${job.id}`, { function: 'processNextJob', job: job, objectId: objectId }); + log.verbose(`Finished processing job id ${job.id}`, { + function: 'processNextJob', + job: job, + objectId: objectId + }); this.isBusy = false; // If job is completed, check if there are more jobs if (!this.toClose) this.checkQueue(); } } catch (err) { - log.error(`Error encountered on job id ${job.id}: ${err.message}`, { function: 'processNextJob', job: job, error: err }); + log.error(`Error encountered on job id ${job.id}: ${err.message}`, { + function: 'processNextJob', + job: job, + error: err + }); const maxRetries = parseInt(config.get('server.maxRetries')); if (job.retries + 1 > maxRetries) { - log.warn(`Job has exceeded the ${maxRetries} maximum retries permitted`, { function: 'processNextJob', job: job, maxRetries: maxRetries }); + log.warn(`Job has exceeded the ${maxRetries} maximum retries permitted`, { + function: 'processNextJob', + job: job, + maxRetries: maxRetries + }); } else { objectQueueService.enqueue({ jobs: [{ bucketId: job.bucketId, path: job.path }], diff --git a/app/src/controllers/bucket.js b/app/src/controllers/bucket.js index 1cc31cea..5d8ee28f 100644 --- a/app/src/controllers/bucket.js +++ b/app/src/controllers/bucket.js @@ -133,6 +133,69 @@ const controller = { } }, + /** + * @function createBucketChild + * Creates a child bucket + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next The next callback function + * @returns {function} Express middleware function + * @throws The error encountered upon failure + */ + async createBucketChild(req, res, next) { + try { + // Get Parent bucket data + const parentBucketId = addDashesToUuid(req.params.bucketId); + const parentBucket = await bucketService.read(parentBucketId); + + // Check new child key length + const childKey = joinPath(stripDelimit(parentBucket.key), stripDelimit(req.body.subKey)); + if (childKey.length > 255) { + throw new Problem(422, { + detail: 'New derived key exceeds maximum length of 255', + instance: req.originalUrl, + key: childKey + }); + } + + // Check for existing bucket collision + const bucketCollision = await bucketService.readUnique({ + bucket: parentBucket.bucket, + endpoint: parentBucket.endpoint, + key: childKey + }).catch(() => undefined); + + if (bucketCollision) { + throw new Problem(409, { + bucketId: bucketCollision.bucketId, + detail: 'Requested bucket already exists', + instance: req.originalUrl, + key: childKey + }); + } + + // Check for credential accessibility/validity + const childBucket = { + bucketName: req.body.bucketName, + accessKeyId: parentBucket.accessKeyId, + bucket: parentBucket.bucket, + endpoint: parentBucket.endpoint, + key: childKey, + secretAccessKey: parentBucket.secretAccessKey, + region: parentBucket.region ?? undefined, + active: parentBucket.active + }; + await controller._validateCredentials(childBucket); + childBucket.userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); + + // Create child bucket + const response = await bucketService.create(childBucket); + res.status(201).json(redactSecrets(response, secretFields)); + } catch (e) { + next(errorToProblem(SERVICE, e)); + } + }, + /** * @function deleteBucket * Deletes the bucket diff --git a/app/src/controllers/bucketPermission.js b/app/src/controllers/bucketPermission.js index 3bb2388e..10a5caee 100644 --- a/app/src/controllers/bucketPermission.js +++ b/app/src/controllers/bucketPermission.js @@ -90,7 +90,9 @@ const controller = { async addPermissions(req, res, next) { try { const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); - const response = await bucketPermissionService.addPermissions(addDashesToUuid(req.params.bucketId), req.body, userId); + const response = await bucketPermissionService.addPermissions( + addDashesToUuid(req.params.bucketId),req.body, userId + ); res.status(201).json(response); } catch (e) { next(errorToProblem(SERVICE, e)); diff --git a/app/src/controllers/object.js b/app/src/controllers/object.js index 49c0ee73..dddd851a 100644 --- a/app/src/controllers/object.js +++ b/app/src/controllers/object.js @@ -149,8 +149,15 @@ const controller = { await utils.trxWrapper(async (trx) => { // create or update version in DB (if a non-versioned object) const version = s3Response.VersionId ? - await versionService.copy(sourceS3VersionId, s3Response.VersionId, objId, s3Response.CopyObjectResult?.ETag, userId, trx) : - await versionService.update({ ...data, id: objId, etag: s3Response.CopyObjectResult?.ETag, isLatest: true }, userId, trx); + await versionService.copy( + sourceS3VersionId, s3Response.VersionId, objId, s3Response.CopyObjectResult?.ETag, userId, trx + ) : + await versionService.update({ + ...data, + id: objId, + etag: s3Response.CopyObjectResult?.ETag, + isLatest: true + }, userId, trx); // add metadata for version in DB await metadataService.associateMetadata(version.id, getKeyValue(data.metadata), userId, trx); @@ -183,9 +190,15 @@ const controller = { // format new tags to array of objects const newTags = Object.entries({ ...req.query.tagset }).map(([k, v]) => ({ Key: k, Value: v })); // get source version that we are adding tags to - const sourceS3VersionId = await getS3VersionId(req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId); + const sourceS3VersionId = await getS3VersionId( + req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId + ); // get existing tags on source version - const { TagSet: existingTags } = await storageService.getObjectTagging({ filePath: objPath, s3VersionId: sourceS3VersionId, bucketId: bucketId }); + const { TagSet: existingTags } = await storageService.getObjectTagging({ + filePath: objPath, + s3VersionId: sourceS3VersionId, + bucketId: bucketId + }); const newSet = newTags // Join new tags and existing tags @@ -243,7 +256,11 @@ const controller = { // Preflight CREATE permission check if bucket scoped and OIDC authenticated const bucketId = req.query.bucketId ? addDashesToUuid(req.query.bucketId) : undefined; if (bucketId && userId) { - const permission = await bucketPermissionService.searchPermissions({ userId: userId, bucketId: bucketId, permCode: 'CREATE' }); + const permission = await bucketPermissionService.searchPermissions({ + userId: userId, + bucketId: bucketId, + permCode: 'CREATE' + }); if (!permission.length) { throw new Problem(403, { detail: 'User lacks permission to complete this action', @@ -381,7 +398,9 @@ const controller = { const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); // Source S3 Version to copy from - const sourceS3VersionId = await getS3VersionId(req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId); + const sourceS3VersionId = await getS3VersionId( + req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId + ); const source = await storageService.headObject({ filePath: objPath, s3VersionId: sourceS3VersionId, bucketId }); if (source.ContentLength > MAXCOPYOBJECTLENGTH) { @@ -428,8 +447,15 @@ const controller = { await utils.trxWrapper(async (trx) => { // create or update version in DB(if a non-versioned object) const version = s3Response.VersionId ? - await versionService.copy(sourceS3VersionId, s3Response.VersionId, objId, s3Response.CopyObjectResult?.ETag, userId, trx) : - await versionService.update({ ...data, id: objId, etag: s3Response.CopyObjectResult?.ETag, isLatest: true }, userId, trx); + await versionService.copy( + sourceS3VersionId, s3Response.VersionId, objId, s3Response.CopyObjectResult?.ETag, userId, trx + ) : + await versionService.update({ + ...data, + id: objId, + etag: s3Response.CopyObjectResult?.ETag, + isLatest: true + }, userId, trx); // add metadata to version in DB await metadataService.associateMetadata(version.id, getKeyValue(data.metadata), userId, trx); @@ -457,7 +483,9 @@ const controller = { const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); // target S3 version to delete - const targetS3VersionId = await getS3VersionId(req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId); + const targetS3VersionId = await getS3VersionId( + req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId + ); const data = { bucketId: req.currentObject?.bucketId, @@ -519,9 +547,15 @@ const controller = { const objPath = req.currentObject?.path; // Target S3 version - const targetS3VersionId = await getS3VersionId(req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId); + const targetS3VersionId = await getS3VersionId( + req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId + ); - const sourceObject = await storageService.getObjectTagging({ filePath: objPath, s3VersionId: targetS3VersionId, bucketId: bucketId }); + const sourceObject = await storageService.getObjectTagging({ + filePath: objPath, + s3VersionId: targetS3VersionId, + bucketId: bucketId + }); // Generate object subset by subtracting/omitting defined keys via filter/inclusion const keysToRemove = req.query.tagset ? Object.keys(req.query.tagset) : []; @@ -633,7 +667,9 @@ const controller = { const objId = addDashesToUuid(req.params.objectId); // target S3 version - const targetS3VersionId = await getS3VersionId(req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId); + const targetS3VersionId = await getS3VersionId( + req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId + ); const data = { bucketId: req.currentObject?.bucketId, @@ -692,7 +728,9 @@ const controller = { const objId = addDashesToUuid(req.params.objectId); // target S3 version - const targetS3VersionId = await getS3VersionId(req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId); + const targetS3VersionId = await getS3VersionId( + req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId + ); const data = { // TODO: use req.currentObject.bucketId @@ -703,7 +741,8 @@ const controller = { // Download via service proxy if (req.query.download && req.query.download === DownloadMode.PROXY) { - // TODO: Consider if we need a HEAD operation first before doing the actual read on large files for pre-flight caching behavior? + // TODO: Consider if we need a HEAD operation first before doing the actual read on large files + // for pre-flight caching behavior? const response = await storageService.readObject(data); // Set Headers via CORS library @@ -756,7 +795,9 @@ const controller = { const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); // source S3 version - const sourceS3VersionId = await getS3VersionId(req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId); + const sourceS3VersionId = await getS3VersionId( + req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId + ); // get metadata for source version const source = await storageService.headObject({ filePath: objPath, s3VersionId: sourceS3VersionId, bucketId }); @@ -795,9 +836,15 @@ const controller = { await utils.trxWrapper(async (trx) => { // create or update version (if a non-versioned object) const version = s3Response.VersionId ? - await versionService.copy(sourceS3VersionId, s3Response.VersionId, objId, s3Response.CopyObjectResult?.ETag, userId, trx) : - - await versionService.update({ ...data, id: objId, etag: s3Response.CopyObjectResult?.ETag, isLatest: true }, userId, trx); + await versionService.copy( + sourceS3VersionId, s3Response.VersionId, objId, s3Response.CopyObjectResult?.ETag, userId, trx + ) : + await versionService.update({ + ...data, + id: objId, + etag: s3Response.CopyObjectResult?.ETag, + isLatest: true + }, userId, trx); // add metadata await metadataService.associateMetadata(version.id, getKeyValue(data.metadata), userId, trx); @@ -829,7 +876,9 @@ const controller = { const newTags = Object.entries({ ...req.query.tagset }).map(([k, v]) => ({ Key: k, Value: v })); // source S3 version - const sourceS3VersionId = await getS3VersionId(req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId); + const sourceS3VersionId = await getS3VersionId( + req.query.s3VersionId, addDashesToUuid(req.query.versionId), objId + ); const data = { bucketId: req.currentObject?.bucketId, @@ -1040,10 +1089,14 @@ const controller = { object.versionId = version.id; // Update Metadata - if (data.metadata && Object.keys(data.metadata).length) await metadataService.associateMetadata(version.id, getKeyValue(data.metadata), userId, trx); + if (data.metadata && Object.keys(data.metadata).length) { + await metadataService.associateMetadata(version.id, getKeyValue(data.metadata), userId, trx); + } // Update Tags - if (data.tags && Object.keys(data.tags).length) await tagService.replaceTags(version.id, getKeyValue(data.tags), userId, trx); + if (data.tags && Object.keys(data.tags).length) { + await tagService.replaceTags(version.id, getKeyValue(data.tags), userId, trx); + } return object; }); diff --git a/app/src/controllers/objectPermission.js b/app/src/controllers/objectPermission.js index dcb30a6c..2c6fa64d 100644 --- a/app/src/controllers/objectPermission.js +++ b/app/src/controllers/objectPermission.js @@ -88,7 +88,9 @@ const controller = { async addPermissions(req, res, next) { try { const userId = await userService.getCurrentUserId(utils.getCurrentIdentity(req.currentUser, SYSTEM_USER)); - const response = await objectPermissionService.addPermissions(utils.addDashesToUuid(req.params.objectId), req.body, userId); + const response = await objectPermissionService.addPermissions( + utils.addDashesToUuid(req.params.objectId), req.body, userId + ); res.status(201).json(response); } catch (e) { next(errorToProblem(SERVICE, e)); diff --git a/app/src/db/dataConnection.js b/app/src/db/dataConnection.js index 8be34667..44c930ab 100644 --- a/app/src/db/dataConnection.js +++ b/app/src/db/dataConnection.js @@ -62,10 +62,14 @@ class DataConnection { log.verbose(`Connect OK: ${connectOk}, Schema OK: ${schemaOk}, Models OK: ${modelsOk}`, { function: 'checkAll' }); if (!connectOk) { - log.error('Could not connect to the database, check configuration and ensure database server is running', { function: 'checkAll' }); + log.error('Could not connect to the database, check configuration and ensure database server is running', { + function: 'checkAll' + }); } if (!schemaOk) { - log.error('Connected to the database, could not verify the schema. Ensure proper migrations have been run.', { function: 'checkAll' }); + log.error('Connected to the database, could not verify the schema. Ensure proper migrations have been run.', { + function: 'checkAll' + }); } if (!modelsOk) { log.error('Connected to the database, schema is ok, could not initialize Knex Models.', { function: 'checkAll' }); diff --git a/app/src/db/migrations/20220130133615_000-init.js b/app/src/db/migrations/20220130133615_000-init.js index 87c70b91..222f2f73 100644 --- a/app/src/db/migrations/20220130133615_000-init.js +++ b/app/src/db/migrations/20220130133615_000-init.js @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ const stamps = require('../stamps'); const { NIL: SYSTEM_USER } = require('uuid'); diff --git a/app/src/db/migrations/20220627000000_002-metadata-tags.js b/app/src/db/migrations/20220627000000_002-metadata-tags.js index e95add78..1e9a8618 100644 --- a/app/src/db/migrations/20220627000000_002-metadata-tags.js +++ b/app/src/db/migrations/20220627000000_002-metadata-tags.js @@ -18,7 +18,8 @@ exports.up = function (knex) { .then(() => knex.schema.createTable('version_metadata', table => { table.primary(['versionId', 'metadataId']); table.uuid('versionId').notNullable().references('id').inTable('version').onDelete('CASCADE').onUpdate('CASCADE'); - table.integer('metadataId').notNullable().references('id').inTable('metadata').onDelete('CASCADE').onUpdate('CASCADE'); + table.integer('metadataId').notNullable().references('id').inTable('metadata').onDelete('CASCADE') + .onUpdate('CASCADE'); stamps(knex, table); })) diff --git a/app/src/db/migrations/20221014000000_003-multi-bucket.js b/app/src/db/migrations/20221014000000_003-multi-bucket.js index cc406c36..f1b1e384 100644 --- a/app/src/db/migrations/20221014000000_003-multi-bucket.js +++ b/app/src/db/migrations/20221014000000_003-multi-bucket.js @@ -18,9 +18,11 @@ exports.up = function (knex) { })) .then(() => knex.schema.createTable('bucket_permission', table => { table.uuid('id').primary(); - table.uuid('bucketId').references('bucketId').inTable('bucket').notNullable().onUpdate('CASCADE').onDelete('CASCADE'); + table.uuid('bucketId').references('bucketId').inTable('bucket').notNullable().onUpdate('CASCADE') + .onDelete('CASCADE'); table.uuid('userId').references('userId').inTable('user').notNullable().onUpdate('CASCADE').onDelete('CASCADE'); - table.string('permCode').references('permCode').inTable('permission').notNullable().onUpdate('CASCADE').onDelete('CASCADE'); + table.string('permCode').references('permCode').inTable('permission').notNullable().onUpdate('CASCADE') + .onDelete('CASCADE'); stamps(knex, table); })) diff --git a/app/src/db/models/utils.js b/app/src/db/models/utils.js index 9f324514..9a37a127 100644 --- a/app/src/db/models/utils.js +++ b/app/src/db/models/utils.js @@ -59,7 +59,9 @@ const utils = { toArray(values) { if (values) { - return Array.isArray(values) ? values.filter(p => p && p.trim().length > 0) : [values].filter(p => p && p.trim().length > 0); + return Array.isArray(values) + ? values.filter(p => p && p.trim().length > 0) + : [values].filter(p => p && p.trim().length > 0); } return []; }, diff --git a/app/src/docs/v1.api-spec.yaml b/app/src/docs/v1.api-spec.yaml index 9891bfef..c888ce95 100644 --- a/app/src/docs/v1.api-spec.yaml +++ b/app/src/docs/v1.api-spec.yaml @@ -201,6 +201,68 @@ paths: $ref: "#/components/responses/Forbidden" default: $ref: "#/components/responses/Error" + /bucket/{bucketId}/child: + put: + summary: Creates a child bucket + description: >- + Creates a child bucket record relative to the parent bucket and copies + the majority of the credential values. Bucket should exist in S3. This + endpoint will report a collision and the bucketId it collided with if + the desired bucket already exists. User requires `MANAGE` permission + on the parent bucket to proceed. + operationId: createBucketChild + tags: + - Bucket + parameters: + - $ref: "#/components/parameters/Path-BucketId" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Request-CreateBucketChild" + responses: + "201": + description: Returns inserted DB child bucket record + content: + application/json: + schema: + $ref: "#/components/schemas/DB-Bucket" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "409": + description: Conflict (Request conflicts with server state) + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Response-Conflict" + - type: object + properties: + bucketId: + type: string + description: The bucketId this request is in collision with + example: ac246e31-c807-496c-bc93-cd8bc2f1b2b4 + key: + type: string + description: The derived bucket key + example: foo/bar + "422": + description: Unprocessable Content (Derived bucket key is too long) + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Response-ValidationError" + - type: object + properties: + key: + type: string + description: The derived bucket key + example: foo/bar + default: + $ref: "#/components/responses/Error" /bucket/{bucketId}/sync: get: summary: Synchronize a bucket @@ -2147,6 +2209,26 @@ components: type: string default: / example: coms/env + Request-CreateBucketChild: + title: Request Create Bucket Child + type: object + required: + - bucketName + - subKey + allOf: + - type: object + properties: + bucketName: + type: string + description: A human-readable label or title + maxLength: 255 + example: Geospatial Maps + subKey: + type: string + description: >- + The desired S3 child directory name. Must not include `/`. + maxLength: 255 + example: child Request-UpdateBucket: title: Request Update Bucket type: object diff --git a/app/src/middleware/authorization.js b/app/src/middleware/authorization.js index 9f8aa7d7..7b42f7ef 100644 --- a/app/src/middleware/authorization.js +++ b/app/src/middleware/authorization.js @@ -136,7 +136,10 @@ const hasPermission = (permission) => { } } catch (err) { log.verbose(err.message, { function: 'hasPermission' }); - return next(new Problem(403, { detail: 'User lacks permission to complete this action', instance: req.originalUrl })); + return next(new Problem(403, { + detail: 'User lacks permission to complete this action', + instance: req.originalUrl + })); } next(); diff --git a/app/src/routes/v1/bucket.js b/app/src/routes/v1/bucket.js index 4b85cc67..9016de89 100644 --- a/app/src/routes/v1/bucket.js +++ b/app/src/routes/v1/bucket.js @@ -17,9 +17,8 @@ router.put('/', express.json(), bucketValidator.createBucket, (req, res, next) = /** * Returns bucket headers - * Notes: - * - router.head() should appear before router.get() method using same path, otherwise router.get() will be called instead. - * - if bucketId path param is not given, router.get('/') (the bucket search endpoint) is called instead. + * router.head() must be declared before router.get() - otherwise router.get() will be called instead. + * If bucketId path param is not given, router.get('/') (the bucket search endpoint) is called instead. */ router.head('/:bucketId', bucketValidator.headBucket, hasPermission(Permissions.READ), (req, res, next) => { bucketController.headBucket(req, res, next); @@ -36,15 +35,24 @@ router.get('/', bucketValidator.searchBuckets, (req, res, next) => { }); /** Updates a bucket */ -router.patch('/:bucketId', express.json(), bucketValidator.updateBucket, hasPermission(Permissions.UPDATE), (req, res, next) => { - bucketController.updateBucket(req, res, next); -}); +router.patch('/:bucketId', express.json(), bucketValidator.updateBucket, hasPermission(Permissions.UPDATE), + (req, res, next) => { + bucketController.updateBucket(req, res, next); + } +); /** Deletes the bucket */ router.delete('/:bucketId', bucketValidator.deleteBucket, hasPermission(Permissions.DELETE), (req, res, next) => { bucketController.deleteBucket(req, res, next); }); +/** Creates a child bucket */ +router.put('/:bucketId/child', express.json(), bucketValidator.createBucketChild, hasPermission(Permissions.MANAGE), + (req, res, next) => { + bucketController.createBucketChild(req, res, next); + } +); + /** Synchronizes a bucket */ router.get('/:bucketId/sync', bucketValidator.syncBucket, hasPermission(Permissions.READ), (req, res, next) => { syncController.syncBucket(req, res, next); diff --git a/app/src/routes/v1/docs.js b/app/src/routes/v1/docs.js index 3fe041ad..4774e126 100644 --- a/app/src/routes/v1/docs.js +++ b/app/src/routes/v1/docs.js @@ -10,6 +10,7 @@ function getSpec() { const spec = yaml.load(rawSpec); spec.servers[0].url = '/api/v1'; if (config.has('keycloak.enabled')) { + // eslint-disable-next-line max-len spec.components.securitySchemes.OpenID.openIdConnectUrl = `${config.get('keycloak.serverUrl')}/realms/${config.get('keycloak.realm')}/.well-known/openid-configuration`; } return spec; diff --git a/app/src/routes/v1/object.js b/app/src/routes/v1/object.js index 1145200d..ea79610c 100644 --- a/app/src/routes/v1/object.js +++ b/app/src/routes/v1/object.js @@ -30,69 +30,95 @@ router.get('/tagging', requireSomeAuth, objectValidator.fetchTags, (req, res, ne }); /** Returns object headers */ -router.head('/:objectId', objectValidator.headObject, currentObject, hasPermission(Permissions.READ), (req, res, next) => { - objectController.headObject(req, res, next); -}); +router.head('/:objectId', objectValidator.headObject, currentObject, hasPermission(Permissions.READ), + (req, res, next) => { + objectController.headObject(req, res, next); + } +); /** Returns the object */ -router.get('/:objectId', objectValidator.readObject, currentObject, hasPermission(Permissions.READ), (req, res, next) => { +router.get('/:objectId', objectValidator.readObject, currentObject, hasPermission(Permissions.READ), + (req, res, next) => { // TODO: Add validation to reject unexpected query parameters - objectController.readObject(req, res, next); -}); + objectController.readObject(req, res, next); + } +); /** Updates an object */ -router.put('/:objectId', requireSomeAuth, objectValidator.updateObject, currentUpload(), currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { - objectController.updateObject(req, res, next); -}); +router.put('/:objectId', requireSomeAuth, objectValidator.updateObject, currentUpload(), + currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { + objectController.updateObject(req, res, next); + } +); /** Deletes the object */ -router.delete('/:objectId', requireSomeAuth, objectValidator.deleteObject, currentObject, hasPermission(Permissions.DELETE), (req, res, next) => { - objectController.deleteObject(req, res, next); -}); +router.delete('/:objectId', requireSomeAuth, objectValidator.deleteObject, + currentObject, hasPermission(Permissions.DELETE), (req, res, next) => { + objectController.deleteObject(req, res, next); + } +); /** Returns the object version history */ -router.get('/:objectId/version', requireSomeAuth, objectValidator.listObjectVersion, currentObject, hasPermission(Permissions.READ), (req, res, next) => { - objectController.listObjectVersion(req, res, next); -}); +router.get('/:objectId/version', requireSomeAuth, objectValidator.listObjectVersion, + currentObject, hasPermission(Permissions.READ), (req, res, next) => { + objectController.listObjectVersion(req, res, next); + } +); /** Sets the public flag of an object */ -router.patch('/:objectId/public', requireSomeAuth, objectValidator.togglePublic, currentObject, hasPermission(Permissions.MANAGE), (req, res, next) => { - objectController.togglePublic(req, res, next); -}); +router.patch('/:objectId/public', requireSomeAuth, objectValidator.togglePublic, + currentObject, hasPermission(Permissions.MANAGE), (req, res, next) => { + objectController.togglePublic(req, res, next); + } +); /** Add metadata to an object */ -router.patch('/:objectId/metadata', requireSomeAuth, objectValidator.addMetadata, currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { - objectController.addMetadata(req, res, next); -}); +router.patch('/:objectId/metadata', requireSomeAuth, objectValidator.addMetadata, + currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { + objectController.addMetadata(req, res, next); + } +); /** Replace metadata on an object */ -router.put('/:objectId/metadata', requireSomeAuth, objectValidator.replaceMetadata, currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { - objectController.replaceMetadata(req, res, next); -}); +router.put('/:objectId/metadata', requireSomeAuth, objectValidator.replaceMetadata, + currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { + objectController.replaceMetadata(req, res, next); + } +); /** Deletes an objects metadata */ -router.delete('/:objectId/metadata', requireSomeAuth, objectValidator.deleteMetadata, currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { - objectController.deleteMetadata(req, res, next); -}); +router.delete('/:objectId/metadata', requireSomeAuth, objectValidator.deleteMetadata, + currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { + objectController.deleteMetadata(req, res, next); + } +); /** Synchronizes an object */ -router.get('/:objectId/sync', requireSomeAuth, objectValidator.syncObject, currentObject, hasPermission(Permissions.READ), (req, res, next) => { - syncController.syncObject(req, res, next); -}); +router.get('/:objectId/sync', requireSomeAuth, objectValidator.syncObject, + currentObject, hasPermission(Permissions.READ), (req, res, next) => { + syncController.syncObject(req, res, next); + } +); /** Add tags to an object */ -router.patch('/:objectId/tagging', requireSomeAuth, objectValidator.addTags, currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { - objectController.addTags(req, res, next); -}); +router.patch('/:objectId/tagging', requireSomeAuth, objectValidator.addTags, + currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { + objectController.addTags(req, res, next); + } +); /** Add tags to an object */ -router.put('/:objectId/tagging', requireSomeAuth, objectValidator.replaceTags, currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { - objectController.replaceTags(req, res, next); -}); +router.put('/:objectId/tagging', requireSomeAuth, objectValidator.replaceTags, + currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { + objectController.replaceTags(req, res, next); + } +); /** Add tags to an object */ -router.delete('/:objectId/tagging', requireSomeAuth, objectValidator.deleteTags, currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { - objectController.deleteTags(req, res, next); -}); +router.delete('/:objectId/tagging', requireSomeAuth, objectValidator.deleteTags, + currentObject, hasPermission(Permissions.UPDATE), (req, res, next) => { + objectController.deleteTags(req, res, next); + } +); module.exports = router; diff --git a/app/src/routes/v1/permission/bucketPermission.js b/app/src/routes/v1/permission/bucketPermission.js index 765d572f..48b7972b 100644 --- a/app/src/routes/v1/permission/bucketPermission.js +++ b/app/src/routes/v1/permission/bucketPermission.js @@ -16,18 +16,24 @@ router.get('/', bucketPermissionValidator.searchPermissions, (req, res, next) => }); /** Returns the bucket permissions */ -router.get('/:bucketId', bucketPermissionValidator.listPermissions, currentObject, hasPermission(Permissions.READ), (req, res, next) => { - bucketPermissionController.listPermissions(req, res, next); -}); +router.get('/:bucketId', bucketPermissionValidator.listPermissions, currentObject, hasPermission(Permissions.READ), + (req, res, next) => { + bucketPermissionController.listPermissions(req, res, next); + } +); /** Grants bucket permissions to users */ -router.put('/:bucketId', express.json(), bucketPermissionValidator.addPermissions, currentObject, hasPermission(Permissions.MANAGE), (req, res, next) => { - bucketPermissionController.addPermissions(req, res, next); -}); +router.put('/:bucketId', express.json(), bucketPermissionValidator.addPermissions, + currentObject, hasPermission(Permissions.MANAGE), (req, res, next) => { + bucketPermissionController.addPermissions(req, res, next); + } +); /** Deletes bucket permissions for a user */ -router.delete('/:bucketId', bucketPermissionValidator.removePermissions, currentObject, hasPermission(Permissions.MANAGE), (req, res, next) => { - bucketPermissionController.removePermissions(req, res, next); -}); +router.delete('/:bucketId', bucketPermissionValidator.removePermissions, + currentObject, hasPermission(Permissions.MANAGE), (req, res, next) => { + bucketPermissionController.removePermissions(req, res, next); + } +); module.exports = router; diff --git a/app/src/routes/v1/permission/objectPermission.js b/app/src/routes/v1/permission/objectPermission.js index 431e5c05..fb4a436b 100644 --- a/app/src/routes/v1/permission/objectPermission.js +++ b/app/src/routes/v1/permission/objectPermission.js @@ -16,18 +16,24 @@ router.get('/', objectPermissionValidator.searchPermissions, (req, res, next) => }); /** Returns the object permissions */ -router.get('/:objectId', objectPermissionValidator.listPermissions, currentObject, hasPermission(Permissions.MANAGE), (req, res, next) => { - objectPermissionController.listPermissions(req, res, next); -}); +router.get('/:objectId', objectPermissionValidator.listPermissions, currentObject, hasPermission(Permissions.MANAGE), + (req, res, next) => { + objectPermissionController.listPermissions(req, res, next); + } +); /** Grants object permissions to users */ -router.put('/:objectId', express.json(), objectPermissionValidator.addPermissions, currentObject, hasPermission(Permissions.MANAGE), (req, res, next) => { - objectPermissionController.addPermissions(req, res, next); -}); +router.put('/:objectId', express.json(), objectPermissionValidator.addPermissions, + currentObject, hasPermission(Permissions.MANAGE), (req, res, next) => { + objectPermissionController.addPermissions(req, res, next); + } +); /** Deletes object permissions for a user */ -router.delete('/:objectId', objectPermissionValidator.removePermissions, currentObject, hasPermission(Permissions.MANAGE), (req, res, next) => { - objectPermissionController.removePermissions(req, res, next); -}); +router.delete('/:objectId', objectPermissionValidator.removePermissions, + currentObject, hasPermission(Permissions.MANAGE), (req, res, next) => { + objectPermissionController.removePermissions(req, res, next); + } +); module.exports = router; diff --git a/app/src/services/metadata.js b/app/src/services/metadata.js index c2d3f833..bfd457b2 100644 --- a/app/src/services/metadata.js +++ b/app/src/services/metadata.js @@ -11,7 +11,8 @@ const service = { * Makes the incoming list of metadata the definitive set associated with versionId * Dissociaate extraneous metadata and also does collision detection for null versions (non-versioned) * @param {string} versionId The uuid id column from version table - * @param {object[]} metadata Incoming array of metadata objects to add for this version (eg: [{ key: 'a', value: '1'}, {key: 'B', value: '2'}]). + * @param {object[]} metadata Incoming array of metadata objects to add for this version + * (eg: [{ key: 'a', value: '1'}, {key: 'B', value: '2'}]). * This will always be the definitive metadata we want on the version * @param {string} [currentUserId=SYSTEM_USER] The optional userId uuid actor; defaults to system user if unspecified * @param {object} [etrx=undefined] An optional Objection Transaction object @@ -33,7 +34,9 @@ const service = { .modify('filterVersionId', versionId); // remove existing joins for metadata that is not in incomming set if (associatedMetadata.length) { - const dissociateMetadata = associatedMetadata.filter(({ metadataId }) => !dbMetadata.some(({ id }) => id === metadataId)); + const dissociateMetadata = associatedMetadata.filter(({ metadataId }) => { + return !dbMetadata.some(({ id }) => id === metadataId); + }); if (dissociateMetadata.length) { await VersionMetadata.query(trx) .whereIn('metadataId', dissociateMetadata.map(vm => vm.metadataId)) @@ -43,7 +46,9 @@ const service = { } // join new metadata - const newJoins = associatedMetadata.length ? dbMetadata.filter(({ id }) => !associatedMetadata.some(({ metadataId }) => metadataId === id)) : dbMetadata; + const newJoins = associatedMetadata.length + ? dbMetadata.filter(({ id }) => !associatedMetadata.some(({ metadataId }) => metadataId === id)) + : dbMetadata; if (newJoins.length) { response = await VersionMetadata.query(trx) diff --git a/app/src/services/storage.js b/app/src/services/storage.js index aa2c37b0..dcf2ec5c 100644 --- a/app/src/services/storage.js +++ b/app/src/services/storage.js @@ -275,10 +275,13 @@ const objectStorageService = { * @param {string} [options.filePath=undefined] Optional filePath of the objects * @param {string} [options.bucketId=undefined] Optional bucketId * @param {boolean} [options.precisePath=true] Optional boolean for filtering results based on the precise path - * @param {boolean} [options.filterLatest=false] Optional boolean for filtering results to only entries with IsLatest being true + * @param {boolean} [options.filterLatest=false] Optional boolean for filtering results to only entries + * with IsLatest being true * @returns {Promise} An object containg an array of DeleteMarkers and Versions */ - async listAllObjectVersions({ filePath = undefined, bucketId = undefined, precisePath = true, filterLatest = false } = {}) { + async listAllObjectVersions({ + filePath = undefined, bucketId = undefined, precisePath = true, filterLatest = false + } = {}) { const key = filePath ?? (await utils.getBucket(bucketId)).key; const path = key !== DELIMITER ? key : ''; @@ -320,7 +323,9 @@ const objectStorageService = { * @param {string} [options.bucketId=undefined] Optional bucketId * @returns {Promise} The response of the list objects v2 operation */ - async listObjectsV2({ filePath = undefined, continuationToken = undefined, maxKeys = undefined, bucketId = undefined } = {}) { + async listObjectsV2({ + filePath = undefined, continuationToken = undefined, maxKeys = undefined, bucketId = undefined + } = {}) { const data = await utils.getBucket(bucketId); const prefix = data.key !== DELIMITER ? data.key : ''; const params = { @@ -342,7 +347,9 @@ const objectStorageService = { * @param {string} [options.bucketId=undefined] Optional bucketId * @returns {Promise} The response of the list object version operation */ - async listObjectVersion({ filePath = undefined, keyMarker = undefined, maxKeys = undefined, bucketId = undefined } = {}) { + async listObjectVersion({ + filePath = undefined, keyMarker = undefined, maxKeys = undefined, bucketId = undefined + } = {}) { const data = await utils.getBucket(bucketId); const prefix = data.key !== DELIMITER ? data.key : ''; const params = { @@ -492,7 +499,8 @@ const objectStorageService = { * @param {object} [options.metadata] Optional object containing key/value pairs for metadata * @param {object} [options.tags] Optional object containing key/value pairs for tags * @param {string} [options.bucketId] Optional bucketId - * @returns {Promise} The response of the put object operation + * @returns {Promise} + * The response of the put object operation */ async upload({ stream, name, length, mimeType, metadata, tags, bucketId = undefined }) { const data = await utils.getBucket(bucketId); diff --git a/app/src/services/sync.js b/app/src/services/sync.js index a0402214..c35ad324 100644 --- a/app/src/services/sync.js +++ b/app/src/services/sync.js @@ -30,7 +30,8 @@ const service = { let objId = uuidv4(); if (typeof s3Object === 'object') { // If regular S3 Object - const TagSet = await storageService.getObjectTagging({ filePath: path, bucketId: bucketId }).then(result => result.TagSet ?? []); + const TagSet = await storageService.getObjectTagging({ filePath: path, bucketId: bucketId }) + .then(result => result.TagSet ?? []); const s3ObjectComsId = TagSet.find(obj => (obj.Key === 'coms-id'))?.Value; if (s3ObjectComsId && uuidValidate(s3ObjectComsId)) { diff --git a/app/src/services/tag.js b/app/src/services/tag.js index 77e58fec..aed4d097 100644 --- a/app/src/services/tag.js +++ b/app/src/services/tag.js @@ -11,7 +11,8 @@ const service = { * @function replaceTags * Makes the incoming list of tags the definitive set associated with versionId * @param {string} versionId The uuid id column from version table - * @param {object[]} tags Incoming array of tageset objects to add for this version (eg: [{ key: 'a', value: '1'}, {key: 'B', value: '2'}]) + * @param {object[]} tags Incoming array of tageset objects to add for this version + * (eg: [{ key: 'a', value: '1'}, {key: 'B', value: '2'}]) * @param {string} [currentUserId=SYSTEM_USER] The optional userId uuid actor; defaults to system user if unspecified * @param {object} [etrx=undefined] An optional Objection Transaction object * @returns {Promise} The result of running the insert operation @@ -221,7 +222,8 @@ const service = { /** * @function fetchTagsForObject - * Fetch matching tags on latest version of provided objects, optionally scoped to a user's object/bucket READ permission + * Fetch matching tags on latest version of provided objects, + * optionally scoped to a user's object/bucket READ permission * @param {string[]} [params.bucketIds] An array of uuids representing buckets * @param {string[]} [params.objectIds] An array of uuids representing objects * @param {object} [params.tagset] Optional object of tags key/value pairs diff --git a/app/src/validators/bucket.js b/app/src/validators/bucket.js index 212d3c02..5062f87f 100644 --- a/app/src/validators/bucket.js +++ b/app/src/validators/bucket.js @@ -10,13 +10,23 @@ const schema = { accessKeyId: Joi.string().max(255).required(), bucket: Joi.string().max(255).required(), endpoint: Joi.string().uri({ scheme: /https?/ }).max(255).required(), - key: Joi.string().trim().max(255), + key: Joi.string().max(255).trim().strict(), secretAccessKey: Joi.string().max(255).required(), region: Joi.string().max(255), active: type.truthy }).required(), }, + createBucketChild: { + body: Joi.object().keys({ + bucketName: Joi.string().max(255).required(), + subKey: Joi.string().max(255).trim().strict().pattern(/^[^/]+$/).required() + }).required(), + params: Joi.object({ + bucketId: type.uuidv4 + }) + }, + deleteBucket: { params: Joi.object({ bucketId: type.uuidv4 @@ -68,6 +78,7 @@ const schema = { const validator = { createBucket: validate(schema.createBucket), + createBucketChild: validate(schema.createBucketChild), deleteBucket: validate(schema.deleteBucket), headBucket: validate(schema.headBucket), readBucket: validate(schema.readBucket), diff --git a/app/tests/unit/controllers/bucket.spec.js b/app/tests/unit/controllers/bucket.spec.js index f33b6b06..ffbd6d45 100644 --- a/app/tests/unit/controllers/bucket.spec.js +++ b/app/tests/unit/controllers/bucket.spec.js @@ -218,6 +218,237 @@ describe('createBucket', () => { }); }); +describe('createBucketChild', () => { + const USR_IDENTITY = 'xxxy'; + const USR_ID = 'abc-123'; + + // mock service calls + const createSpy = jest.spyOn(bucketService, 'create'); + const getCurrentIdentitySpy = jest.spyOn(utils, 'getCurrentIdentity'); + const getCurrentUserIdSpy = jest.spyOn(userService, 'getCurrentUserId'); + const headBucketSpy = jest.spyOn(storageService, 'headBucket'); + const readSpy = jest.spyOn(bucketService, 'read'); + const readUniqueSpy = jest.spyOn(bucketService, 'readUnique'); + + const next = jest.fn(); + + it('should return a 201 and redacts secrets in the response', async () => { + const req = { + body: { bucketName: 'bucketName', subKey: 'subKey' }, + currentUser: CURRENT_USER, + headers: {}, + params: { bucketId: REQUEST_BUCKET_ID }, + query: {}, + }; + + createSpy.mockResolvedValue({ + accessKeyId: 'no no no', + secretAccessKey: 'absolutely not', + other: 'some field', + xyz: 123, + }); + getCurrentIdentitySpy.mockReturnValue(USR_IDENTITY); + getCurrentUserIdSpy.mockReturnValue(USR_ID); + readSpy.mockResolvedValue({ + accessKeyId: 'accessKeyId', + bucket: 'bucket', + endpoint: 'endpoint', + key: 'key', + secretAccessKey: 'secretAccessKey', + region: 'region', + active: true + }); + headBucketSpy.mockReturnValue(true); + readUniqueSpy.mockRejectedValue(false); + utils.addDashesToUuid.mockReturnValue(REQUEST_BUCKET_ID); + utils.joinPath.mockReturnValue('key/subKey'); + + await controller.createBucketChild(req, res, next); + + expect(createSpy).toHaveBeenCalledTimes(1); + expect(createSpy).toHaveBeenCalledWith(expect.objectContaining({ + bucketName: 'bucketName', + accessKeyId: 'accessKeyId', + bucket: 'bucket', + endpoint: 'endpoint', + key: 'key/subKey', + secretAccessKey: 'secretAccessKey', + region: 'region', + active: true + })); + expect(getCurrentIdentitySpy).toHaveBeenCalledTimes(1); + expect(getCurrentIdentitySpy).toHaveBeenCalledWith( + CURRENT_USER, + SYSTEM_USER + ); + expect(getCurrentUserIdSpy).toHaveBeenCalledTimes(1); + expect(getCurrentUserIdSpy).toHaveBeenCalledWith(USR_IDENTITY); + expect(headBucketSpy).toHaveBeenCalledTimes(1); + expect(headBucketSpy).toHaveBeenCalledWith(expect.objectContaining({ + accessKeyId: 'accessKeyId', + bucket: 'bucket', + endpoint: 'endpoint', + key: 'key/subKey', + region: 'region', + secretAccessKey: 'secretAccessKey' + })); + expect(readSpy).toHaveBeenCalledTimes(1); + expect(readSpy).toHaveBeenCalledWith(REQUEST_BUCKET_ID); + expect(readUniqueSpy).toHaveBeenCalledTimes(1); + expect(readUniqueSpy).toHaveBeenCalledWith(expect.objectContaining({ + bucket: 'bucket', + endpoint: 'endpoint', + key: 'key/subKey', + })); + + expect(res.status).toHaveBeenCalledWith(201); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + accessKeyId: 'REDACTED', + secretAccessKey: 'REDACTED' + }) + ); + }); + + it('should return a 409 when bucket already exists', async () => { + const req = { + body: { bucketName: 'bucketName', subKey: 'subKey' }, + currentUser: CURRENT_USER, + headers: {}, + params: { bucketId: REQUEST_BUCKET_ID }, + query: {}, + }; + + readSpy.mockResolvedValue({ + accessKeyId: 'accessKeyId', + bucket: 'bucket', + endpoint: 'endpoint', + key: 'key', + secretAccessKey: 'secretAccessKey', + region: 'region', + active: true + }); + readUniqueSpy.mockResolvedValue({ bucketId: REQUEST_BUCKET_ID }); + utils.addDashesToUuid.mockReturnValue(REQUEST_BUCKET_ID); + utils.joinPath.mockReturnValue('key/subKey'); + + await controller.createBucketChild(req, res, next); + + expect(createSpy).toHaveBeenCalledTimes(0); + expect(getCurrentIdentitySpy).toHaveBeenCalledTimes(0); + expect(getCurrentUserIdSpy).toHaveBeenCalledTimes(0); + expect(headBucketSpy).toHaveBeenCalledTimes(0); + expect(readSpy).toHaveBeenCalledTimes(1); + expect(readSpy).toHaveBeenCalledWith(REQUEST_BUCKET_ID); + expect(readUniqueSpy).toHaveBeenCalledTimes(1); + expect(readUniqueSpy).toHaveBeenCalledWith(expect.objectContaining({ + bucket: 'bucket', + endpoint: 'endpoint', + key: 'key/subKey', + })); + + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(new Problem(409, { + detail: 'Requested bucket already exists', + bucketId: REQUEST_BUCKET_ID, + key: 'key/subKey' + })); + }); + + it('should return a 409 when bucket can not be validated', async () => { + const req = { + body: { bucketName: 'bucketName', subKey: 'subKey' }, + currentUser: CURRENT_USER, + headers: {}, + params: { bucketId: REQUEST_BUCKET_ID }, + query: {}, + }; + + headBucketSpy.mockRejectedValue(false); + readSpy.mockResolvedValue({ + accessKeyId: 'accessKeyId', + bucket: 'bucket', + endpoint: 'endpoint', + key: 'key', + secretAccessKey: 'secretAccessKey', + region: 'region', + active: true + }); + readUniqueSpy.mockRejectedValue(false); + utils.addDashesToUuid.mockReturnValue(REQUEST_BUCKET_ID); + utils.joinPath.mockReturnValue('key/subKey'); + + await controller.createBucketChild(req, res, next); + + expect(createSpy).toHaveBeenCalledTimes(0); + expect(getCurrentIdentitySpy).toHaveBeenCalledTimes(0); + expect(getCurrentUserIdSpy).toHaveBeenCalledTimes(0); + expect(headBucketSpy).toHaveBeenCalledTimes(1); + expect(headBucketSpy).toHaveBeenCalledWith(expect.objectContaining({ + accessKeyId: 'accessKeyId', + bucket: 'bucket', + endpoint: 'endpoint', + key: 'key/subKey', + region: 'region', + secretAccessKey: 'secretAccessKey' + })); + expect(readSpy).toHaveBeenCalledTimes(1); + expect(readSpy).toHaveBeenCalledWith(REQUEST_BUCKET_ID); + expect(readUniqueSpy).toHaveBeenCalledTimes(1); + expect(readUniqueSpy).toHaveBeenCalledWith(expect.objectContaining({ + bucket: 'bucket', + endpoint: 'endpoint', + key: 'key/subKey', + })); + + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(new Problem(409, { + detail: 'Unable to validate supplied credentials for the bucket' + })); + }); + + it('should return a 422 when derived key is too long', async () => { + const req = { + body: { bucketName: 'bucketName', subKey: 'subKey' }, + currentUser: CURRENT_USER, + headers: {}, + params: { bucketId: REQUEST_BUCKET_ID }, + query: {}, + }; + + readSpy.mockResolvedValue({ + accessKeyId: 'accessKeyId', + bucket: 'bucket', + endpoint: 'endpoint', + key: 'key', + secretAccessKey: 'secretAccessKey', + region: 'region', + active: true + }); + utils.addDashesToUuid.mockReturnValue(REQUEST_BUCKET_ID); + utils.joinPath.mockReturnValue('01234567890123456789012345678901234567890123456789012345678901234567890123456789\ + 01234567890123456789012345678901234567890123456789012345678901234567890123456789\ + 01234567890123456789012345678901234567890123456789012345678901234567890123456789\ + 01234567890123456789012345678901234567890123456789012345678901234567890123456789'); + + await controller.createBucketChild(req, res, next); + + expect(createSpy).toHaveBeenCalledTimes(0); + expect(getCurrentIdentitySpy).toHaveBeenCalledTimes(0); + expect(getCurrentUserIdSpy).toHaveBeenCalledTimes(0); + expect(headBucketSpy).toHaveBeenCalledTimes(0); + expect(readSpy).toHaveBeenCalledTimes(1); + expect(readSpy).toHaveBeenCalledWith(REQUEST_BUCKET_ID); + expect(readUniqueSpy).toHaveBeenCalledTimes(0); + + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(new Problem(422, { + detail: 'New derived key exceeds maximum length of 255', + key: expect.any(String) + })); + }); +}); + describe('deleteBucket', () => { // mock service calls const addDashesToUuidSpy = jest.spyOn(utils, 'addDashesToUuid'); diff --git a/app/tests/unit/db/models/utils.spec.js b/app/tests/unit/db/models/utils.spec.js index 028ed392..44219e5a 100644 --- a/app/tests/unit/db/models/utils.spec.js +++ b/app/tests/unit/db/models/utils.spec.js @@ -84,7 +84,8 @@ describe('inArrayFilter', () => { it('should return the desired clause for multiple values joined with OR', () => { const col = 'user'; const vals = ['1', '2', '3']; - expect(inArrayFilter(col, vals)).toEqual('(array_length("user", 1) > 0 and (\'1\' = ANY("user") or \'2\' = ANY("user") or \'3\' = ANY("user")))'); + expect(inArrayFilter(col, vals)) + .toEqual('(array_length("user", 1) > 0 and (\'1\' = ANY("user") or \'2\' = ANY("user") or \'3\' = ANY("user")))'); }); }); diff --git a/app/tests/unit/middleware/authentication.spec.js b/app/tests/unit/middleware/authentication.spec.js index 75447a7d..a614101a 100644 --- a/app/tests/unit/middleware/authentication.spec.js +++ b/app/tests/unit/middleware/authentication.spec.js @@ -72,6 +72,7 @@ describe('_checkBasicAuth', () => { describe('_spkiWrapper', () => { it('returns the PEM format we expect', () => { + // eslint-disable-next-line max-len const spki = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4CcG7WPTCF4YLHxT3bs9ilcQ6SS+A2e/PiZ9hqR0noelBCsdW0SQGOhjE7nhl2lrZ0W/o80YKMzNZ42Hmc7p0sHU3RN95OCTHvyCazC/CKM2i+gD+cAspP/Ns+hOqNmxC/XIsgD3bZ2zobNMhNy3jgDaAsbs3kOGPIwkdo/vWeo7N6fZPxOgSp6JoGBDtehuyhQ/4y2f7TnyicIvHMuc2d7Bz4GalQ/ra+GspmZ/HqL93A6c8sDHa8fqC8O+gnzpBNsCOxJcq/i3NOaGrOFMCiJwsNVc2dUcY8epcW3pwakIRLlC6D7oawbxv7c3UsXoCt4XSC0hdjwXg5kxVXHoDQIDAQAB'; const result = mw._spkiWrapper(spki); diff --git a/app/tests/unit/middleware/authorization.spec.js b/app/tests/unit/middleware/authorization.spec.js index 3a8f2b3e..7516515e 100644 --- a/app/tests/unit/middleware/authorization.spec.js +++ b/app/tests/unit/middleware/authorization.spec.js @@ -55,82 +55,116 @@ describe('_checkPermission', () => { [false, Permissions.READ, { objectId: SYSTEM_USER }, [], []], [false, Permissions.READ, { objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], []], [true, Permissions.READ, { objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }], []], - [true, Permissions.READ, { objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }], []], + [true, Permissions.READ, { objectId: SYSTEM_USER }, [ + { permCode: Permissions.READ }, { permCode: Permissions.UPDATE } + ], []], [false, Permissions.READ, { bucketId: SYSTEM_USER }, [], []], [false, Permissions.READ, { bucketId: SYSTEM_USER }, [], [{ permCode: Permissions.UPDATE }]], [true, Permissions.READ, { bucketId: SYSTEM_USER }, [], [{ permCode: Permissions.READ }]], - [true, Permissions.READ, { bucketId: SYSTEM_USER }, [], [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }]], + [true, Permissions.READ, { bucketId: SYSTEM_USER }, [], [ + { permCode: Permissions.READ }, { permCode: Permissions.UPDATE } + ]], [false, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [], []], [false, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], []], [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }], []], - [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }], []], - [false, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], [{ permCode: Permissions.UPDATE }]], - [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], [{ permCode: Permissions.READ }]], - [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }], [{ permCode: Permissions.UPDATE }]], - [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }], [{ permCode: Permissions.UPDATE }]], - [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }]], - ])('should return %s given a bucketless object, permission %s, params %j, objPerms %j and bucketPerms %j', async (expected, permission, params, objPerms, bucketPerms) => { - const req = { - currentObject: {}, - currentUser: { authType: AuthType.BEARER }, - params: params - }; - getCurrentIdentitySpy.mockReturnValue(SYSTEM_USER); - getCurrentUserIdSpy.mockResolvedValue(SYSTEM_USER); - bucketSearchPermissionsSpy.mockResolvedValue(bucketPerms); - objSearchPermissionsSpy.mockResolvedValue(objPerms); - - const result = await mw._checkPermission(req, permission); - - expect(result).toBe(expected); - expect(getCurrentIdentitySpy).toHaveBeenCalledTimes(1); - expect(getCurrentIdentitySpy).toHaveBeenCalledWith(req.currentUser, SYSTEM_USER); - expect(getCurrentUserIdSpy).toHaveBeenCalledTimes(1); - expect(getCurrentUserIdSpy).toHaveBeenCalledWith(SYSTEM_USER); - expect(bucketSearchPermissionsSpy).toHaveBeenCalledTimes(params.bucketId ? 1 : 0); - expect(objSearchPermissionsSpy).toHaveBeenCalledTimes(params.objectId ? 1 : 0); - }); + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [ + { permCode: Permissions.READ }, { permCode: Permissions.UPDATE } + ], []], + [false, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [ + { permCode: Permissions.UPDATE }], [{ permCode: Permissions.UPDATE } + ]], + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [ + { permCode: Permissions.UPDATE }], [{ permCode: Permissions.READ } + ]], + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [ + { permCode: Permissions.READ }], [{ permCode: Permissions.UPDATE } + ]], + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [ + { permCode: Permissions.READ }, { permCode: Permissions.UPDATE }], [{ permCode: Permissions.UPDATE } + ]], + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [ + { permCode: Permissions.UPDATE }], [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE } + ]], + ])('should return %s given a bucketless object, permission %s, params %j, objPerms %j and bucketPerms %j', + async (expected, permission, params, objPerms, bucketPerms) => { + const req = { + currentObject: {}, + currentUser: { authType: AuthType.BEARER }, + params: params + }; + getCurrentIdentitySpy.mockReturnValue(SYSTEM_USER); + getCurrentUserIdSpy.mockResolvedValue(SYSTEM_USER); + bucketSearchPermissionsSpy.mockResolvedValue(bucketPerms); + objSearchPermissionsSpy.mockResolvedValue(objPerms); + + const result = await mw._checkPermission(req, permission); + + expect(result).toBe(expected); + expect(getCurrentIdentitySpy).toHaveBeenCalledTimes(1); + expect(getCurrentIdentitySpy).toHaveBeenCalledWith(req.currentUser, SYSTEM_USER); + expect(getCurrentUserIdSpy).toHaveBeenCalledTimes(1); + expect(getCurrentUserIdSpy).toHaveBeenCalledWith(SYSTEM_USER); + expect(bucketSearchPermissionsSpy).toHaveBeenCalledTimes(params.bucketId ? 1 : 0); + expect(objSearchPermissionsSpy).toHaveBeenCalledTimes(params.objectId ? 1 : 0); + }); it.each([ [false, Permissions.READ, {}, [], []], [false, Permissions.READ, { objectId: SYSTEM_USER }, [], []], [false, Permissions.READ, { objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], []], [true, Permissions.READ, { objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }], []], - [true, Permissions.READ, { objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }], []], + [true, Permissions.READ, { objectId: SYSTEM_USER }, [ + { permCode: Permissions.READ }, { permCode: Permissions.UPDATE } + ], []], [false, Permissions.READ, { bucketId: SYSTEM_USER }, [], []], [false, Permissions.READ, { bucketId: SYSTEM_USER }, [], [{ permCode: Permissions.UPDATE }]], [true, Permissions.READ, { bucketId: SYSTEM_USER }, [], [{ permCode: Permissions.READ }]], - [true, Permissions.READ, { bucketId: SYSTEM_USER }, [], [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }]], + [true, Permissions.READ, { bucketId: SYSTEM_USER }, [], [ + { permCode: Permissions.READ }, { permCode: Permissions.UPDATE } + ]], [false, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [], []], [false, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], []], [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }], []], - [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }], []], - [false, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], [{ permCode: Permissions.UPDATE }]], - [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], [{ permCode: Permissions.READ }]], - [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }], [{ permCode: Permissions.UPDATE }]], - [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }], [{ permCode: Permissions.UPDATE }]], - [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }]], - ])('should return %s given a bucketed object, permission %s, params %j, objPerms %j and bucketPerms %j', async (expected, permission, params, objPerms, bucketPerms) => { - const req = { - currentObject: { bucketId: SYSTEM_USER }, - currentUser: { authType: AuthType.BEARER }, - params: params - }; - getCurrentIdentitySpy.mockReturnValue(SYSTEM_USER); - getCurrentUserIdSpy.mockResolvedValue(SYSTEM_USER); - bucketSearchPermissionsSpy.mockResolvedValue(bucketPerms); - objSearchPermissionsSpy.mockResolvedValue(objPerms); - - const result = await mw._checkPermission(req, permission); - - expect(result).toBe(expected); - expect(getCurrentIdentitySpy).toHaveBeenCalledTimes(1); - expect(getCurrentIdentitySpy).toHaveBeenCalledWith(req.currentUser, SYSTEM_USER); - expect(getCurrentUserIdSpy).toHaveBeenCalledTimes(1); - expect(getCurrentUserIdSpy).toHaveBeenCalledWith(SYSTEM_USER); - expect(bucketSearchPermissionsSpy).toHaveBeenCalledTimes(1); - expect(objSearchPermissionsSpy).toHaveBeenCalledTimes(params.objectId ? 1 : 0); - }); + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [ + { permCode: Permissions.READ }, { permCode: Permissions.UPDATE } + ], []], + [false, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [ + { permCode: Permissions.UPDATE }], [{ permCode: Permissions.UPDATE } + ]], + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [ + { permCode: Permissions.UPDATE }], [{ permCode: Permissions.READ } + ]], + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [ + { permCode: Permissions.READ }], [{ permCode: Permissions.UPDATE } + ]], + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [ + { permCode: Permissions.READ }, { permCode: Permissions.UPDATE } + ], [{ permCode: Permissions.UPDATE }]], + [true, Permissions.READ, { bucketId: SYSTEM_USER, objectId: SYSTEM_USER }, [{ permCode: Permissions.UPDATE }], [ + { permCode: Permissions.READ }, { permCode: Permissions.UPDATE } + ]], + ])('should return %s given a bucketed object, permission %s, params %j, objPerms %j and bucketPerms %j', + async (expected, permission, params, objPerms, bucketPerms) => { + const req = { + currentObject: { bucketId: SYSTEM_USER }, + currentUser: { authType: AuthType.BEARER }, + params: params + }; + getCurrentIdentitySpy.mockReturnValue(SYSTEM_USER); + getCurrentUserIdSpy.mockResolvedValue(SYSTEM_USER); + bucketSearchPermissionsSpy.mockResolvedValue(bucketPerms); + objSearchPermissionsSpy.mockResolvedValue(objPerms); + + const result = await mw._checkPermission(req, permission); + + expect(result).toBe(expected); + expect(getCurrentIdentitySpy).toHaveBeenCalledTimes(1); + expect(getCurrentIdentitySpy).toHaveBeenCalledWith(req.currentUser, SYSTEM_USER); + expect(getCurrentUserIdSpy).toHaveBeenCalledTimes(1); + expect(getCurrentUserIdSpy).toHaveBeenCalledWith(SYSTEM_USER); + expect(bucketSearchPermissionsSpy).toHaveBeenCalledTimes(1); + expect(objSearchPermissionsSpy).toHaveBeenCalledTimes(params.objectId ? 1 : 0); + }); }); describe('checkAppMode', () => { @@ -389,28 +423,29 @@ describe('hasPermission', () => { [0, AuthType.BEARER, SYSTEM_USER, []], [0, AuthType.BEARER, SYSTEM_USER, [{ permCode: Permissions.UPDATE }]], [1, AuthType.BEARER, SYSTEM_USER, [{ permCode: Permissions.READ }, { permCode: Permissions.UPDATE }]] - ])('should call next %i times when authType %s, userId %o and have permissions %j', async (nextCount, type, userId, perms) => { - const searchPermCount = +(type === AuthType.BEARER && !!userId); - getAppAuthModeSpy.mockReturnValue(AuthMode.OIDCAUTH); - getCurrentUserIdSpy.mockResolvedValue(userId); - objSearchPermissionsSpy.mockResolvedValue(perms); - req.currentObject.public = false; - req.currentUser.authType = type; - req.params.objectId = SYSTEM_USER; - - const result = mw.hasPermission(Permissions.READ); - expect(result).toBeInstanceOf(Function); - await result(req, res, next); - - expect(next).toHaveBeenCalledTimes(1); - if (nextCount) expect(next).toHaveBeenCalledWith(); - else expect(next).toHaveBeenCalledWith(expect.any(Object)); - - expect(objSearchPermissionsSpy).toHaveBeenCalledTimes(searchPermCount); - if (searchPermCount) { - expect(objSearchPermissionsSpy).toHaveBeenCalledWith(expect.objectContaining({ objId: SYSTEM_USER })); - expect(objSearchPermissionsSpy).toHaveBeenCalledWith(expect.objectContaining({ userId: userId })); - } - }); + ])('should call next %i times when authType %s, userId %o and have permissions %j', + async (nextCount, type, userId, perms) => { + const searchPermCount = +(type === AuthType.BEARER && !!userId); + getAppAuthModeSpy.mockReturnValue(AuthMode.OIDCAUTH); + getCurrentUserIdSpy.mockResolvedValue(userId); + objSearchPermissionsSpy.mockResolvedValue(perms); + req.currentObject.public = false; + req.currentUser.authType = type; + req.params.objectId = SYSTEM_USER; + + const result = mw.hasPermission(Permissions.READ); + expect(result).toBeInstanceOf(Function); + await result(req, res, next); + + expect(next).toHaveBeenCalledTimes(1); + if (nextCount) expect(next).toHaveBeenCalledWith(); + else expect(next).toHaveBeenCalledWith(expect.any(Object)); + + expect(objSearchPermissionsSpy).toHaveBeenCalledTimes(searchPermCount); + if (searchPermCount) { + expect(objSearchPermissionsSpy).toHaveBeenCalledWith(expect.objectContaining({ objId: SYSTEM_USER })); + expect(objSearchPermissionsSpy).toHaveBeenCalledWith(expect.objectContaining({ userId: userId })); + } + }); }); }); diff --git a/app/tests/unit/middleware/upload.spec.js b/app/tests/unit/middleware/upload.spec.js index 7419b98f..deb1c5dd 100644 --- a/app/tests/unit/middleware/upload.spec.js +++ b/app/tests/unit/middleware/upload.spec.js @@ -23,42 +23,67 @@ describe('currentUpload', () => { it.each([ [0, undefined, false, undefined, undefined, undefined], [0, undefined, false, 0, undefined, undefined], - [1, { contentLength: 539, filename: undefined, mimeType: 'application/octet-stream' }, false, 539, undefined, undefined], - [1, { contentLength: 539, filename: undefined, mimeType: 'application/octet-stream' }, false, 539, 'inline', undefined], - [1, { contentLength: 539, filename: undefined, mimeType: 'application/octet-stream' }, false, 539, 'xattachment', undefined], - [1, { contentLength: 539, filename: undefined, mimeType: 'application/octet-stream' }, false, 539, 'attachment; xfilename="foo.txt"', undefined], - [1, { contentLength: 539, filename: 'foo.txt', mimeType: 'application/octet-stream' }, false, 539, 'attachment; filename="foo.txt"', undefined], - [1, { contentLength: 539, filename: 'foo.txt', mimeType: 'text/plain' }, false, 539, 'attachment; filename="foo.txt"', 'text/plain'], - [1, { contentLength: 539, filename: 'föo.txt', mimeType: 'text/plain' }, false, 539, 'attachment; filename=foo.txt; filename*=UTF-8\'\'f%C3%B6o.txt', 'text/plain'], - [1, { contentLength: 539, filename: 'föo.txt', mimeType: 'text/plain' }, false, 539, 'attachment; filename*=UTF-8\'\'f%C3%B6o.txt; filename=foo.txt', 'text/plain'], + [1, { + contentLength: 539, filename: undefined, mimeType: 'application/octet-stream' + }, false, 539, undefined, undefined], + [1, { + contentLength: 539, filename: undefined, mimeType: 'application/octet-stream' + }, false, 539, 'inline', undefined], + [1, { + contentLength: 539, filename: undefined, mimeType: 'application/octet-stream' + }, false, 539, 'xattachment', undefined], + [1, { + contentLength: 539, filename: undefined, mimeType: 'application/octet-stream' + }, false, 539, 'attachment; xfilename="foo.txt"', undefined], + [1, { + contentLength: 539, filename: 'foo.txt', mimeType: 'application/octet-stream' + }, false, 539, 'attachment; filename="foo.txt"', undefined], + [1, { + contentLength: 539, filename: 'foo.txt', mimeType: 'text/plain' + }, false, 539, 'attachment; filename="foo.txt"', 'text/plain'], + [1, { + contentLength: 539, filename: 'föo.txt', mimeType: 'text/plain' + }, false, 539, 'attachment; filename=foo.txt; filename*=UTF-8\'\'f%C3%B6o.txt', 'text/plain'], + [1, { + contentLength: 539, filename: 'föo.txt', mimeType: 'text/plain' + }, false, 539, 'attachment; filename*=UTF-8\'\'f%C3%B6o.txt; filename=foo.txt', 'text/plain'], [0, undefined, true, undefined, undefined, undefined], [0, undefined, true, 0, undefined, undefined], [0, undefined, true, 539, undefined, undefined], [0, undefined, true, 539, 'inline', undefined], [0, undefined, true, 539, 'xattachment', undefined], [0, undefined, true, 539, 'attachment; xfilename="foo.txt"', undefined], - [1, { contentLength: 539, filename: 'foo.txt', mimeType: 'application/octet-stream' }, true, 539, 'attachment; filename="foo.txt"', undefined], - [1, { contentLength: 539, filename: 'foo.txt', mimeType: 'text/plain' }, true, 539, 'attachment; filename="foo.txt"', 'text/plain'], - [1, { contentLength: 539, filename: 'föo.txt', mimeType: 'text/plain' }, true, 539, 'attachment; filename=foo.txt; filename*=UTF-8\'\'f%C3%B6o.txt', 'text/plain'], - [1, { contentLength: 539, filename: 'föo.txt', mimeType: 'text/plain' }, true, 539, 'attachment; filename*=UTF-8\'\'f%C3%B6o.txt; filename=foo.txt', 'text/plain'] - ])('should call next %i times with currentUpload %j given strict %j, length %j, disposition %j and type %j', (nextCount, current, strict, length, disposition, type) => { - const sendCount = 1 - nextCount; + [1, { + contentLength: 539, filename: 'foo.txt', mimeType: 'application/octet-stream' + }, true, 539, 'attachment; filename="foo.txt"', undefined], + [1, { + contentLength: 539, filename: 'foo.txt', mimeType: 'text/plain' + }, true, 539, 'attachment; filename="foo.txt"', 'text/plain'], + [1, { + contentLength: 539, filename: 'föo.txt', mimeType: 'text/plain' + }, true, 539, 'attachment; filename=foo.txt; filename*=UTF-8\'\'f%C3%B6o.txt', 'text/plain'], + [1, { + contentLength: 539, filename: 'föo.txt', mimeType: 'text/plain' + }, true, 539, 'attachment; filename*=UTF-8\'\'f%C3%B6o.txt; filename=foo.txt', 'text/plain'] + ])('should call next %i times with currentUpload %j given strict %j, length %j, disposition %j and type %j', + (nextCount, current, strict, length, disposition, type) => { + const sendCount = 1 - nextCount; - req.get.mockReturnValueOnce(length); // contentlength - req.get.mockReturnValueOnce(disposition); // contentdisposition - req.get.mockReturnValueOnce(type); // contenttype + req.get.mockReturnValueOnce(length); // contentlength + req.get.mockReturnValueOnce(disposition); // contentdisposition + req.get.mockReturnValueOnce(type); // contenttype - const result = currentUpload(strict); - expect(result).toBeInstanceOf(Function); - if (sendCount) expect(() => result(req, res, next)).toThrow(); - else expect(() => result(req, res, next)).not.toThrow(); + const result = currentUpload(strict); + expect(result).toBeInstanceOf(Function); + if (sendCount) expect(() => result(req, res, next)).toThrow(); + else expect(() => result(req, res, next)).not.toThrow(); - expect(req.currentUpload).toEqual(current); - expect(next).toHaveBeenCalledTimes(nextCount); - if (nextCount) { - expect(req.socket.server.requestTimeout).toEqual(0); - expect(next).toHaveBeenCalledWith(); - } - }); + expect(req.currentUpload).toEqual(current); + expect(next).toHaveBeenCalledTimes(nextCount); + if (nextCount) { + expect(req.socket.server.requestTimeout).toEqual(0); + expect(next).toHaveBeenCalledWith(); + } + }); }); diff --git a/app/tests/unit/validators/bucket.spec.js b/app/tests/unit/validators/bucket.spec.js index 18168d11..33c5eb6c 100644 --- a/app/tests/unit/validators/bucket.spec.js +++ b/app/tests/unit/validators/bucket.spec.js @@ -84,6 +84,11 @@ describe('createBucket', () => { }), ])); }); + + it('is strict', () => { + expect(key.preferences).toBeTruthy(); + expect(key.preferences.convert).toBeFalsy(); + }); }); it('should match the schema', () => { @@ -108,6 +113,116 @@ describe('createBucket', () => { }); }); +describe('createBucketChild', () => { + + describe('body', () => { + const body = schema.createBucketChild.body.describe(); + + describe('bucketName', () => { + const bucketName = body.keys.bucketName; + + it('is a string', () => { + expect(bucketName).toBeTruthy(); + expect(bucketName.type).toEqual('string'); + }); + + it('has a max length of 255', () => { + expect(Array.isArray(bucketName.rules)).toBeTruthy(); + expect(bucketName.rules).toHaveLength(1); + expect(bucketName.rules).toEqual(expect.arrayContaining([ + expect.objectContaining({ + args: { + limit: 255 + }, + name: 'max' + }), + ])); + }); + + it('is required', () => { + expect(bucketName.flags).toBeTruthy(); + expect(bucketName.flags).toEqual(expect.objectContaining({ + presence: 'required' + })); + }); + }); + + describe.only('key', () => { + const subKey = body.keys.subKey; + + it('is a string', () => { + expect(subKey).toBeTruthy(); + expect(subKey.type).toEqual('string'); + }); + + it('trims whitespace', () => { + expect(Array.isArray(subKey.rules)).toBeTruthy(); + expect(subKey.rules).toHaveLength(3); + expect(subKey.rules).toEqual(expect.arrayContaining([ + expect.objectContaining({ + args: { + enabled: true + }, + name: 'trim' + }), + ])); + }); + + it('has a max length of 255', () => { + expect(Array.isArray(subKey.rules)).toBeTruthy(); + expect(subKey.rules).toHaveLength(3); + expect(subKey.rules).toEqual(expect.arrayContaining([ + expect.objectContaining({ + args: { + limit: 255 + }, + name: 'max' + }), + ])); + }); + + it('has a regex', () => { + expect(Array.isArray(subKey.rules)).toBeTruthy(); + expect(subKey.rules).toHaveLength(3); + expect(subKey.rules).toEqual(expect.arrayContaining([ + expect.objectContaining({ + args: { + regex: '/^[^/]+$/' + }, + name: 'pattern' + }), + ])); + }); + + it('is strict', () => { + expect(subKey.preferences).toBeTruthy(); + expect(subKey.preferences.convert).toBeFalsy(); + }); + + it('is required', () => { + expect(subKey.flags).toBeTruthy(); + expect(subKey.flags).toEqual(expect.objectContaining({ presence: 'required' })); + }); + }); + + it('should match the schema', () => { + const value = { + body: { + bucketName: 'ccc', + subKey: 'subKey' + } + }; + expect(value).toMatchSchema(schema.createBucketChild); + }); + + + it('is required', () => { + expect(body.flags).toBeTruthy(); + expect(body.flags).toEqual(expect.objectContaining({ presence: 'required' })); + }); + }); +}); + describe('deleteBucket', () => { describe('params', () => {