diff --git a/packages/component-invite/config/authsome-mode.js b/packages/component-invite/config/authsome-mode.js index 3d3f15f68fc572760b30c203169dd71f40b1e43a..a2b8bd10f6659146281432af6fc7e66f0adc7790 100644 --- a/packages/component-invite/config/authsome-mode.js +++ b/packages/component-invite/config/authsome-mode.js @@ -153,9 +153,15 @@ async function authenticatedUser(user, operation, object, context) { } } - // only allow the HE to create an invitation - if (operation === 'POST' && get(object, 'type') === 'collection') { - const collection = await context.models.Collection.find(get(object, 'id')) + // only allow the HE to create, delete an invitation, or get inv details` + if ( + ['POST', 'GET', 'DELETE'].includes(operation) && + get(object.collection, 'type') === 'collection' && + object.path.includes('invitations') + ) { + const collection = await context.models.Collection.find( + get(object.collection, 'id'), + ) if (collection.handlingEditor.id === user.id) { return true } diff --git a/packages/component-invite/src/routes/collectionsInvitations/delete.js b/packages/component-invite/src/routes/collectionsInvitations/delete.js index 4ae8d7cfb33538d3deddbbcc0814f588d02a437a..71330dc93cb8a07568eec95f71c509223b4c9ba5 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/delete.js +++ b/packages/component-invite/src/routes/collectionsInvitations/delete.js @@ -5,14 +5,23 @@ const logger = require('@pubsweet/logger') const config = require('config') const userHelper = require('../../helpers/User') const collectionHelper = require('../../helpers/Collection') +const authsomeHelper = require('../../helpers/authsome') const statuses = config.get('statuses') module.exports = models => async (req, res) => { - // const reqUser = await models.User.find(req.user) - // TO DO: authsome const { collectionId, invitationId } = req.params try { const collection = await models.Collection.find(collectionId) + const authsome = authsomeHelper.getAuthsome(models) + const target = { + collection, + path: req.route.path, + } + const canDelete = await authsome.can(req.user, 'DELETE', target) + if (!canDelete) + return res.status(403).json({ + error: 'Unauthorized.', + }) const invitation = await collection.invitations.find( invitation => invitation.id === invitationId, ) diff --git a/packages/component-invite/src/routes/collectionsInvitations/get.js b/packages/component-invite/src/routes/collectionsInvitations/get.js index 091890fe1b902375ff70383978c5354679524a69..2a2148198f8f987a61125d91a6b2a03786ab6ff0 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/get.js +++ b/packages/component-invite/src/routes/collectionsInvitations/get.js @@ -2,6 +2,7 @@ const helpers = require('../../helpers/helpers') const teamHelper = require('../../helpers/Team') const config = require('config') const invitationHelper = require('../../helpers/Invitation') +const authsomeHelper = require('../../helpers/authsome') const configRoles = config.get('roles') module.exports = models => async (req, res) => { @@ -19,6 +20,16 @@ module.exports = models => async (req, res) => { const { collectionId } = req.params try { const collection = await models.Collection.find(collectionId) + const authsome = authsomeHelper.getAuthsome(models) + const target = { + collection, + path: req.route.path, + } + const canGet = await authsome.can(req.user, 'GET', target) + if (!canGet) + return res.status(403).json({ + error: 'Unauthorized.', + }) const members = await teamHelper.getTeamMembersByCollection( collectionId, role, diff --git a/packages/component-invite/src/routes/collectionsInvitations/patch.js b/packages/component-invite/src/routes/collectionsInvitations/patch.js index 16d5b3684bb156edd72b665192c204bda55012d1..88a00ace9c079f9550580381a7746f7067eb2f48 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/patch.js +++ b/packages/component-invite/src/routes/collectionsInvitations/patch.js @@ -14,7 +14,6 @@ module.exports = models => async (req, res) => { logger.error('some parameters are missing') return } - let user = await models.User.find(req.user) try { const collection = await models.Collection.find(collectionId) diff --git a/packages/component-invite/src/routes/collectionsInvitations/post.js b/packages/component-invite/src/routes/collectionsInvitations/post.js index 3775cd1105c77359de6202fff7716a47e12c11f2..d22a23e6266d2c4f431342ffc6a7a5f66f09c134 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/post.js +++ b/packages/component-invite/src/routes/collectionsInvitations/post.js @@ -26,7 +26,6 @@ module.exports = models => async (req, res) => { return } const reqUser = await models.User.find(req.user) - const authsome = authsomeHelper.getAuthsome(models) if (email === reqUser.email && !reqUser.admin) { logger.error(`${reqUser.email} tried to invite his own email`) @@ -44,7 +43,12 @@ module.exports = models => async (req, res) => { }) } - const canPost = await authsome.can(req.user, 'POST', collection) + const authsome = authsomeHelper.getAuthsome(models) + const target = { + collection, + path: req.route.path, + } + const canPost = await authsome.can(req.user, 'POST', target) if (!canPost) return res.status(403).json({ error: 'Unauthorized.', diff --git a/packages/component-invite/src/tests/collectionsInvitations/delete.test.js b/packages/component-invite/src/tests/collectionsInvitations/delete.test.js index 38ca5f0f27a654940ee63b22411a73e2b2b6bbac..a59e8437fa08ac6058e002d0460a2761b2482a77 100644 --- a/packages/component-invite/src/tests/collectionsInvitations/delete.test.js +++ b/packages/component-invite/src/tests/collectionsInvitations/delete.test.js @@ -2,15 +2,18 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' process.env.SUPPRESS_NO_CONFIG_WARNING = true const cloneDeep = require('lodash/cloneDeep') -const httpMocks = require('node-mocks-http') const Model = require('./../helpers/Model') const fixtures = require('./../fixtures/fixtures') +const requests = require('./../helpers/requests') jest.mock('pubsweet-component-mail-service', () => ({ setupRevokeInvitationEmail: jest.fn(), })) -const deletePath = '../../routes/collectionsInvitations/delete' +const path = '../../routes/collectionsInvitations/delete' +const route = { + path: '/api/collections/:collectionId/invitations/:invitationId', +} describe('Delete Collections Invitations route handler', () => { let testFixtures = {} @@ -21,11 +24,15 @@ describe('Delete Collections Invitations route handler', () => { }) it('should return an error when the collection does not exist', async () => { const { editorInChief } = testFixtures.users - const req = httpMocks.createRequest() - req.params.collectionId = 'invalid-id' - req.user = editorInChief.id - const res = httpMocks.createResponse() - await require(deletePath)(models)(req, res) + const res = await requests.sendRequest({ + userId: editorInChief.id, + route, + models, + path, + params: { + collectionId: 'invalid-id', + }, + }) expect(res.statusCode).toBe(404) const data = JSON.parse(res._getData()) expect(data.error).toEqual('collection not found') @@ -33,12 +40,16 @@ describe('Delete Collections Invitations route handler', () => { it('should return an error when the invitation does not exist', async () => { const { editorInChief } = testFixtures.users const { collection } = testFixtures.collections - const req = httpMocks.createRequest() - req.params.collectionId = collection.id - req.params.invitationId = 'invalid-id' - req.user = editorInChief.id - const res = httpMocks.createResponse() - await require(deletePath)(models)(req, res) + const res = await requests.sendRequest({ + userId: editorInChief.id, + route, + models, + path, + params: { + collectionId: collection.id, + invitationId: 'invalid-id', + }, + }) expect(res.statusCode).toBe(404) const data = JSON.parse(res._getData()) expect(data.error).toEqual('Invitation invalid-id not found') @@ -46,12 +57,33 @@ describe('Delete Collections Invitations route handler', () => { it('should return success when the collection and invitation exist', async () => { const { editorInChief } = testFixtures.users const { collection } = testFixtures.collections - const req = httpMocks.createRequest() - req.params.collectionId = collection.id - req.params.invitationId = collection.invitations[0].id - req.user = editorInChief.id - const res = httpMocks.createResponse() - await require(deletePath)(models)(req, res) + const res = await requests.sendRequest({ + userId: editorInChief.id, + route, + models, + path, + params: { + collectionId: collection.id, + invitationId: collection.invitations[0].id, + }, + }) expect(res.statusCode).toBe(200) }) + it('should return an error when the user does not have invitation rights', async () => { + const { author } = testFixtures.users + const { collection } = testFixtures.collections + const res = await requests.sendRequest({ + userId: author.id, + route, + models, + path, + params: { + collectionId: collection.id, + invitationId: collection.invitations[0].id, + }, + }) + expect(res.statusCode).toBe(403) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Unauthorized.') + }) }) diff --git a/packages/component-invite/src/tests/collectionsInvitations/get.test.js b/packages/component-invite/src/tests/collectionsInvitations/get.test.js index 02af456f586966bca3c108701feae086d28709bc..105187c0a834d5c5614b9346318ccdbb715ac8c0 100644 --- a/packages/component-invite/src/tests/collectionsInvitations/get.test.js +++ b/packages/component-invite/src/tests/collectionsInvitations/get.test.js @@ -1,12 +1,15 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' process.env.SUPPRESS_NO_CONFIG_WARNING = true -const httpMocks = require('node-mocks-http') const fixtures = require('./../fixtures/fixtures') const Model = require('./../helpers/Model') const cloneDeep = require('lodash/cloneDeep') +const requests = require('./../helpers/requests') -const getPath = '../../routes/collectionsInvitations/get' +const path = '../../routes/collectionsInvitations/get' +const route = { + path: '/api/collections/:collectionId/invitations/:invitationId?', +} describe('Get collection invitations route handler', () => { let testFixtures = {} let models @@ -17,15 +20,19 @@ describe('Get collection invitations route handler', () => { it('should return success when the request data is correct', async () => { const { editorInChief, handlingEditor } = testFixtures.users const { collection } = testFixtures.collections - const req = httpMocks.createRequest() - req.query = { - role: 'handlingEditor', - userId: handlingEditor.id, - } - req.params.collectionId = collection.id - req.user = editorInChief.id - const res = httpMocks.createResponse() - await require(getPath)(models)(req, res) + const res = await requests.sendRequest({ + userId: editorInChief.id, + route, + models, + path, + query: { + role: 'handlingEditor', + userId: handlingEditor.id, + }, + params: { + collectionId: collection.id, + }, + }) expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) @@ -33,27 +40,31 @@ describe('Get collection invitations route handler', () => { }) it('should return an error when parameters are missing', async () => { const { editorInChief } = testFixtures.users - const req = httpMocks.createRequest() - req.query = {} - req.user = editorInChief.id - const res = httpMocks.createResponse() - await require(getPath)(models)(req, res) + const res = await requests.sendRequest({ + userId: editorInChief.id, + route, + models, + path, + }) expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) expect(data.error).toEqual('Role is required') }) it('should return an error when the collection does not exist', async () => { const { editorInChief, handlingEditor } = testFixtures.users - - const req = httpMocks.createRequest() - req.query = { - role: 'handlingEditor', - userId: handlingEditor.id, - } - req.params.collectionId = 'invalid-id' - req.user = editorInChief.id - const res = httpMocks.createResponse() - await require(getPath)(models)(req, res) + const res = await requests.sendRequest({ + userId: editorInChief.id, + route, + models, + path, + query: { + role: 'handlingEditor', + userId: handlingEditor.id, + }, + params: { + collectionId: 'invalid-id', + }, + }) expect(res.statusCode).toBe(404) const data = JSON.parse(res._getData()) expect(data.error).toEqual('collection not found') @@ -61,34 +72,61 @@ describe('Get collection invitations route handler', () => { it('should return an error when the role is invalid', async () => { const { editorInChief, handlingEditor } = testFixtures.users const { collection } = testFixtures.collections - const req = httpMocks.createRequest() - req.query = { - role: 'invalidRole', - userId: handlingEditor.id, - } - req.params.collectionId = collection.id - req.user = editorInChief.id - const res = httpMocks.createResponse() - await require(getPath)(models)(req, res) + const res = await requests.sendRequest({ + userId: editorInChief.id, + route, + models, + path, + query: { + role: 'invalidRole', + userId: handlingEditor.id, + }, + params: { + collectionId: collection.id, + }, + }) expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) - expect(data.error).toEqual(`Role ${req.query.role} is invalid`) + expect(data.error).toEqual(`Role invalidRole is invalid`) }) it('should return success with an empty array when the collection does not have a the requested role team', async () => { const { editorInChief, handlingEditor } = testFixtures.users const { collection } = testFixtures.collections - const req = httpMocks.createRequest() - req.query = { - role: 'author', - userId: handlingEditor.id, - } delete collection.invitations - req.params.collectionId = collection.id - req.user = editorInChief.id - const res = httpMocks.createResponse() - await require(getPath)(models)(req, res) + const res = await requests.sendRequest({ + userId: editorInChief.id, + route, + models, + path, + query: { + role: 'author', + userId: handlingEditor.id, + }, + params: { + collectionId: collection.id, + }, + }) expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) expect(data).toHaveLength(0) }) + it('should return an error when a user does not have invitation rights', async () => { + const { author } = testFixtures.users + const { collection } = testFixtures.collections + const res = await requests.sendRequest({ + userId: author.id, + route, + models, + path, + query: { + role: 'reviewer', + }, + params: { + collectionId: collection.id, + }, + }) + expect(res.statusCode).toBe(403) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Unauthorized.') + }) }) diff --git a/packages/component-invite/src/tests/collectionsInvitations/post.test.js b/packages/component-invite/src/tests/collectionsInvitations/post.test.js index ec7524458eec88eeed58cb2523243bfaf33acbf3..4d1e3bc51d354e4abd809b93519289121e8757f0 100644 --- a/packages/component-invite/src/tests/collectionsInvitations/post.test.js +++ b/packages/component-invite/src/tests/collectionsInvitations/post.test.js @@ -1,13 +1,13 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' process.env.SUPPRESS_NO_CONFIG_WARNING = true -const httpMocks = require('node-mocks-http') const random = require('lodash/random') const fixtures = require('./../fixtures/fixtures') const Chance = require('chance') const Model = require('./../helpers/Model') const config = require('config') const cloneDeep = require('lodash/cloneDeep') +const requests = require('./../helpers/requests') const configRoles = config.get('roles') @@ -28,8 +28,11 @@ const reqBody = { affiliation: chance.company(), admin: false, } +const route = { + path: '/api/collections/:collectionId/invitations', +} -const postPath = '../../routes/collectionsInvitations/post' +const path = '../../routes/collectionsInvitations/post' describe('Post collections invitations route handler', () => { let testFixtures = {} let body = {} @@ -42,12 +45,14 @@ describe('Post collections invitations route handler', () => { it('should return an error params are missing', async () => { const { admin } = testFixtures.users delete body.email - const req = httpMocks.createRequest({ + const res = await requests.sendRequest({ body, + userId: admin.id, + route, + models, + path, }) - req.user = admin.id - const res = httpMocks.createResponse() - await require(postPath)(models)(req, res) + expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) expect(data.error).toEqual('Email and role are required') @@ -55,17 +60,21 @@ describe('Post collections invitations route handler', () => { it('should return success when the editor in chief invites a handlingEditor with a collection', async () => { const { user, editorInChief } = testFixtures.users const { collection } = testFixtures.collections + body = { email: user.email, role: 'handlingEditor', } - const req = httpMocks.createRequest({ + const res = await requests.sendRequest({ body, + userId: editorInChief.id, + route, + models, + path, + params: { + collectionId: collection.id, + }, }) - req.user = editorInChief.id - req.params.collectionId = collection.id - const res = httpMocks.createResponse() - await require(postPath)(models)(req, res) expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) @@ -78,13 +87,16 @@ describe('Post collections invitations route handler', () => { email: user.email, role: 'reviewer', } - const req = httpMocks.createRequest({ + const res = await requests.sendRequest({ body, + userId: editorInChief.id, + route, + models, + path, + params: { + collectionId: collection.id, + }, }) - req.user = editorInChief.id - req.params.collectionId = collection.id - const res = httpMocks.createResponse() - await require(postPath)(models)(req, res) expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) @@ -94,13 +106,13 @@ describe('Post collections invitations route handler', () => { const { editorInChief } = testFixtures.users body.role = roles[random(0, roles.length - 1)] body.email = editorInChief.email - const req = httpMocks.createRequest({ + const res = await requests.sendRequest({ body, + userId: editorInChief.id, + route, + models, + path, }) - req.user = editorInChief.id - const res = httpMocks.createResponse() - await require(postPath)(models)(req, res) - expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) expect(data.error).toEqual('Cannot invite yourself') @@ -108,12 +120,13 @@ describe('Post collections invitations route handler', () => { it('should return an error when the role is invalid', async () => { const { editorInChief } = testFixtures.users body.role = 'someRandomRole' - const req = httpMocks.createRequest({ + const res = await requests.sendRequest({ body, + userId: editorInChief.id, + route, + models, + path, }) - req.user = editorInChief.id - const res = httpMocks.createResponse() - await require(postPath)(models)(req, res) const data = JSON.parse(res._getData()) expect(data.error).toEqual(`Role ${body.role} is invalid`) }) @@ -124,13 +137,16 @@ describe('Post collections invitations route handler', () => { email: handlingEditor.email, role: 'handlingEditor', } - const req = httpMocks.createRequest({ + const res = await requests.sendRequest({ body, + userId: editorInChief.id, + route, + models, + path, + params: { + collectionId: collection.id, + }, }) - req.user = editorInChief.id - req.params.collectionId = collection.id - const res = httpMocks.createResponse() - await require(postPath)(models)(req, res) expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) @@ -143,13 +159,16 @@ describe('Post collections invitations route handler', () => { email: answerReviewer.email, role: 'reviewer', } - const req = httpMocks.createRequest({ + const res = await requests.sendRequest({ body, + userId: handlingEditor.id, + route, + models, + path, + params: { + collectionId: collection.id, + }, }) - req.user = handlingEditor.id - req.params.collectionId = collection.id - const res = httpMocks.createResponse() - await require(postPath)(models)(req, res) expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) expect(data.error).toEqual( @@ -160,13 +179,16 @@ describe('Post collections invitations route handler', () => { const { author } = testFixtures.users const { collection } = testFixtures.collections - const req = httpMocks.createRequest({ + const res = await requests.sendRequest({ body, + userId: author.id, + route, + models, + path, + params: { + collectionId: collection.id, + }, }) - req.user = author.id - req.params.collectionId = collection.id - const res = httpMocks.createResponse() - await require(postPath)(models)(req, res) expect(res.statusCode).toBe(403) const data = JSON.parse(res._getData()) diff --git a/packages/component-invite/src/tests/helpers/requests.js b/packages/component-invite/src/tests/helpers/requests.js new file mode 100644 index 0000000000000000000000000000000000000000..6696fc7c91db5dff36caf192aae5ac5068fab506 --- /dev/null +++ b/packages/component-invite/src/tests/helpers/requests.js @@ -0,0 +1,24 @@ +const httpMocks = require('node-mocks-http') + +const sendRequest = async ({ + body = {}, + userId, + route, + models, + path, + params = {}, + query = {}, +}) => { + const req = httpMocks.createRequest({ + body, + }) + req.user = userId + req.route = route + req.params = params + req.query = query + const res = httpMocks.createResponse() + await require(path)(models)(req, res) + return res +} + +module.exports = { sendRequest } diff --git a/packages/xpub-faraday/config/authsome-mode.js b/packages/xpub-faraday/config/authsome-mode.js index 3d3f15f68fc572760b30c203169dd71f40b1e43a..a2b8bd10f6659146281432af6fc7e66f0adc7790 100644 --- a/packages/xpub-faraday/config/authsome-mode.js +++ b/packages/xpub-faraday/config/authsome-mode.js @@ -153,9 +153,15 @@ async function authenticatedUser(user, operation, object, context) { } } - // only allow the HE to create an invitation - if (operation === 'POST' && get(object, 'type') === 'collection') { - const collection = await context.models.Collection.find(get(object, 'id')) + // only allow the HE to create, delete an invitation, or get inv details` + if ( + ['POST', 'GET', 'DELETE'].includes(operation) && + get(object.collection, 'type') === 'collection' && + object.path.includes('invitations') + ) { + const collection = await context.models.Collection.find( + get(object.collection, 'id'), + ) if (collection.handlingEditor.id === user.id) { return true }