From c0d050aca14575b0514e954e9f29825854e1671a Mon Sep 17 00:00:00 2001 From: Alexandru Munteanu <alexandru.munt@gmail.com> Date: Tue, 19 Jun 2018 15:28:27 +0300 Subject: [PATCH] refactor authsome. it is awesome now --- .../src/CollectionsInvitations.js | 26 -- .../src/routes/collectionsInvitations/get.js | 80 ------ .../routes/fragmentsRecommendations/post.js | 4 +- .../src/routes/fragmentsUsers/post.js | 1 + .../src/components/Dashboard/DashboardCard.js | 2 +- .../src/components/Reviewers/ReviewerList.js | 5 +- .../components-faraday/src/redux/reviewers.js | 8 +- .../xpub-faraday/config/authsome-helpers.js | 34 ++- packages/xpub-faraday/config/authsome-mode.js | 272 +++++------------- 9 files changed, 120 insertions(+), 312 deletions(-) delete mode 100644 packages/component-invite/src/routes/collectionsInvitations/get.js diff --git a/packages/component-invite/src/CollectionsInvitations.js b/packages/component-invite/src/CollectionsInvitations.js index 117d14f74..2f80dd1b7 100644 --- a/packages/component-invite/src/CollectionsInvitations.js +++ b/packages/component-invite/src/CollectionsInvitations.js @@ -38,32 +38,6 @@ const CollectionsInvitations = app => { authBearer, require(`${routePath}/post`)(app.locals.models), ) - /** - * @api {get} /api/collections/:collectionId/invitations/[:invitationId]?role=:role List collections invitations - * @apiGroup CollectionsInvitations - * @apiParam {id} collectionId Collection id - * @apiParam {id} [invitationId] Invitation id - * @apiParam {String} role The role to search for: handlingEditor - * @apiSuccessExample {json} Success - * HTTP/1.1 200 OK - * [{ - * "name": "John Smith", - * "invitedOn": 1525428890167, - * "respondedOn": 1525428890299, - * "email": "email@example.com", - * "status": "pending", - * "invitationId": "1990881" - * }] - * @apiErrorExample {json} List errors - * HTTP/1.1 403 Forbidden - * HTTP/1.1 400 Bad Request - * HTTP/1.1 404 Not Found - */ - app.get( - `${basePath}/:invitationId?`, - authBearer, - require(`${routePath}/get`)(app.locals.models), - ) /** * @api {delete} /api/collections/:collectionId/invitations/:invitationId Delete invitation * @apiGroup CollectionsInvitations diff --git a/packages/component-invite/src/routes/collectionsInvitations/get.js b/packages/component-invite/src/routes/collectionsInvitations/get.js deleted file mode 100644 index db8832dd4..000000000 --- a/packages/component-invite/src/routes/collectionsInvitations/get.js +++ /dev/null @@ -1,80 +0,0 @@ -const config = require('config') -const { - services, - Team, - Invitation, - authsome: authsomeHelper, -} = require('pubsweet-component-helper-service') - -const configRoles = config.get('roles') - -module.exports = models => async (req, res) => { - const { role } = req.query - if (!services.checkForUndefinedParams(role)) { - res.status(400).json({ error: 'Role is required' }) - return - } - - if (!configRoles.collection.includes(role)) { - res.status(400).json({ error: `Role ${role} is invalid` }) - return - } - - const { collectionId } = req.params - const teamHelper = new Team({ TeamModel: models.Team, collectionId }) - - 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.getTeamMembers({ - role, - objectType: 'collection', - }) - if (!members) return res.status(200).json([]) - - // TO DO: handle case for when the invitationID is provided - const invitationHelper = new Invitation({ role }) - - const membersData = members.map(async member => { - const user = await models.User.find(member) - invitationHelper.userId = user.id - const { - invitedOn, - respondedOn, - status, - id, - } = invitationHelper.getInvitationsData({ - invitations: collection.invitations, - }) - - return { - name: `${user.firstName} ${user.lastName}`, - invitedOn, - respondedOn, - email: user.email, - status, - userId: user.id, - invitationId: id, - } - }) - - const resBody = await Promise.all(membersData) - res.status(200).json(resBody) - } catch (e) { - const notFoundError = await services.handleNotFoundError(e, 'collection') - return res.status(notFoundError.status).json({ - error: notFoundError.message, - }) - } -} diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js index 6a7d8cf88..29e3bec20 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js @@ -33,10 +33,8 @@ module.exports = models => async (req, res) => { }) } const authsome = authsomeHelper.getAuthsome(models) - const authsomeObject = - recommendationType === 'editorRecommendation' ? collection : fragment const target = { - authsomeObject, + fragment, path: req.route.path, } const canPost = await authsome.can(req.user, 'POST', target) diff --git a/packages/component-user-manager/src/routes/fragmentsUsers/post.js b/packages/component-user-manager/src/routes/fragmentsUsers/post.js index e298dbebc..7be2f60ea 100644 --- a/packages/component-user-manager/src/routes/fragmentsUsers/post.js +++ b/packages/component-user-manager/src/routes/fragmentsUsers/post.js @@ -12,6 +12,7 @@ const authorKeys = [ 'affiliation', ] +// TODO: add authsome module.exports = models => async (req, res) => { const { email, role, isSubmitting, isCorresponding } = req.body diff --git a/packages/components-faraday/src/components/Dashboard/DashboardCard.js b/packages/components-faraday/src/components/Dashboard/DashboardCard.js index 9ba22a5d7..723635e65 100644 --- a/packages/components-faraday/src/components/Dashboard/DashboardCard.js +++ b/packages/components-faraday/src/components/Dashboard/DashboardCard.js @@ -188,9 +188,9 @@ export default compose( withTheme, connect((state, { project, version }) => ({ isHE: currentUserIs(state, 'handlingEditor'), - invitation: selectInvitation(state, version.id), canMakeDecision: canMakeDecision(state, project), canInviteReviewers: canInviteReviewers(state, project), + invitation: selectInvitation(state, get(version, 'id')), canMakeRecommendation: canMakeRecommendation(state, project), })), )(DashboardCard) diff --git a/packages/components-faraday/src/components/Reviewers/ReviewerList.js b/packages/components-faraday/src/components/Reviewers/ReviewerList.js index 5acbf18c4..4290a947c 100644 --- a/packages/components-faraday/src/components/Reviewers/ReviewerList.js +++ b/packages/components-faraday/src/components/Reviewers/ReviewerList.js @@ -93,6 +93,7 @@ export default compose( withHandlers({ showConfirmResend: ({ showModal, + versionId, collectionId, inviteReviewer, goBackToReviewers, @@ -104,6 +105,7 @@ export default compose( inviteReviewer( pick(reviewer, ['email', 'firstName', 'lastName', 'affiliation']), collectionId, + versionId, ).then(goBackToReviewers, goBackToReviewers) }, onCancel: goBackToReviewers, @@ -112,6 +114,7 @@ export default compose( showConfirmRevoke: ({ showModal, hideModal, + versionId, collectionId, revokeReviewer, goBackToReviewers, @@ -120,7 +123,7 @@ export default compose( title: 'Unassign Reviewer', confirmText: 'Unassign', onConfirm: () => { - revokeReviewer(invitationId, collectionId).then( + revokeReviewer(invitationId, collectionId, versionId).then( goBackToReviewers, goBackToReviewers, ) diff --git a/packages/components-faraday/src/redux/reviewers.js b/packages/components-faraday/src/redux/reviewers.js index 8611d6f38..acc31e864 100644 --- a/packages/components-faraday/src/redux/reviewers.js +++ b/packages/components-faraday/src/redux/reviewers.js @@ -148,10 +148,14 @@ export const setReviewerPassword = reviewerBody => dispatch => { }) } -export const revokeReviewer = (invitationId, collectionId) => dispatch => { +export const revokeReviewer = ( + invitationId, + collectionId, + fragmentId, +) => dispatch => { dispatch(inviteRequest()) return remove( - `/collections/${collectionId}/invitations/${invitationId}`, + `/collections/${collectionId}/fragments/${fragmentId}/invitations/${invitationId}`, ).then( () => dispatch(inviteSuccess()), err => { diff --git a/packages/xpub-faraday/config/authsome-helpers.js b/packages/xpub-faraday/config/authsome-helpers.js index f2cb6e483..3373de304 100644 --- a/packages/xpub-faraday/config/authsome-helpers.js +++ b/packages/xpub-faraday/config/authsome-helpers.js @@ -80,9 +80,38 @@ const heIsInvitedToFragment = async ({ user, Team, collectionId }) => t => t.members.includes(user.id) && t.object.id === collectionId, ) -const getUserPermissions = async ({ user, Team, mapFn = x => x }) => +const getUserPermissions = async ({ + user, + Team, + mapFn = t => ({ + objectId: t.object.id, + objectType: t.object.type, + role: t.teamType.permissions, + }), +}) => (await Promise.all(user.teams.map(teamId => Team.find(teamId)))).map(mapFn) +const isOwner = ({ user: { id }, object }) => { + if (object.owners.includes(id)) return true + return !!object.owners.find(own => own.id === id) +} + +const hasPermissionForObject = async ({ user, object, Team }) => { + const userPermissions = await getUserPermissions({ + user, + Team, + }) + + return !!userPermissions.find( + p => + p.objectId === get(object, 'fragment.id') || + p.objectId === get(object, 'fragment.collectionId'), + ) +} + +const isHandlingEditor = ({ user, object }) => + get(object, 'collection.handlingEditor.id') === user.id + module.exports = { filterObjectData, parseAuthorsData, @@ -90,6 +119,9 @@ module.exports = { getTeamsByPermissions, filterRefusedInvitations, // + isOwner, + isHandlingEditor, getUserPermissions, heIsInvitedToFragment, + hasPermissionForObject, } diff --git a/packages/xpub-faraday/config/authsome-mode.js b/packages/xpub-faraday/config/authsome-mode.js index 7d779340c..8da1d852c 100644 --- a/packages/xpub-faraday/config/authsome-mode.js +++ b/packages/xpub-faraday/config/authsome-mode.js @@ -1,71 +1,8 @@ -const { get, pickBy, omit } = require('lodash') const config = require('config') - -const helpers = require('./authsome-helpers') +const { get, pickBy, omit } = require('lodash') const statuses = config.get('statuses') - -async function teamPermissions(user, operation, object, context) { - const { models } = context - const permissions = ['handlingEditor', 'author', 'reviewer'] - const teams = await helpers.getTeamsByPermissions( - user.teams, - permissions, - context.models.Team, - ) - - let collectionsPermissions = await Promise.all( - teams.map(async team => { - let collection - if (team.object.type === 'collection') { - collection = await models.Collection.find(team.object.id) - } else if (team.object.type === 'fragment') { - const fragment = await models.Fragment.find(team.object.id) - collection = await models.Collection.find(fragment.collectionId) - } - if ( - collection.status === 'rejected' && - team.teamType.permissions === 'reviewer' - ) - return null - const collPerm = { - id: collection.id, - permission: team.teamType.permissions, - } - const objectType = get(object, 'type') - if (objectType === 'fragment') { - if (collection.fragments.includes(object.id)) - collPerm.fragmentId = object.id - else return null - } - - if (objectType === 'collection') - if (object.id !== collection.id) return null - return collPerm - }), - ) - collectionsPermissions = collectionsPermissions.filter(cp => cp !== null) - if (collectionsPermissions.length === 0) return false - - return { - filter: filterParam => { - if (!filterParam.length) { - return helpers.filterObjectData( - collectionsPermissions, - filterParam, - user, - ) - } - - const collections = filterParam - .map(coll => - helpers.filterObjectData(collectionsPermissions, coll, user), - ) - .filter(Boolean) - return collections - }, - } -} +const helpers = require('./authsome-helpers') function unauthenticatedUser(operation, object) { // Public/unauthenticated users can GET /collections, filtered by 'published' @@ -114,15 +51,9 @@ function unauthenticatedUser(operation, object) { } const publicStatusesPermissions = ['author', 'reviewer'] +const createPaths = ['/collections', '/collections/:collectionId/fragments'] async function authenticatedUser(user, operation, object, context) { - // Allow the authenticated user to POST a collection (but not with a 'filtered' property) - if (operation === 'POST' && object.path === '/collections') { - return { - filter: collection => omit(collection, 'filtered'), - } - } - if (operation === 'GET') { if (get(object, 'path') === '/collections') { return { @@ -130,11 +61,6 @@ async function authenticatedUser(user, operation, object, context) { const userPermissions = await helpers.getUserPermissions({ user, Team: context.models.Team, - mapFn: t => ({ - objectId: t.object.id, - objectType: t.object.type, - permissions: t.teamType.permissions, - }), }) return collections.filter(collection => { if (collection.owners.includes(user.id)) { @@ -159,18 +85,20 @@ async function authenticatedUser(user, operation, object, context) { } } + if (object === '/users') { + return true + } + if (get(object, 'type') === 'collection') { + if (helpers.isOwner({ user, object })) { + return true + } return { filter: async collection => { const status = get(collection, 'status') || 'draft' const userPermissions = await helpers.getUserPermissions({ user, Team: context.models.Team, - mapFn: t => ({ - objectId: t.object.id, - objectType: t.object.type, - permissions: t.teamType.permissions, - }), }) if (collection.owners.map(o => o.id).includes(user.id)) { return collection @@ -179,7 +107,11 @@ async function authenticatedUser(user, operation, object, context) { const collectionPermission = userPermissions.find( p => p.objectId === collection.id, ) - if (publicStatusesPermissions.includes(get(collectionPermission))) { + if ( + publicStatusesPermissions.includes( + get(collectionPermission, 'role'), + ) + ) { collection.visibleStatus = statuses[status].public } return collection @@ -188,14 +120,13 @@ async function authenticatedUser(user, operation, object, context) { } if (get(object, 'type') === 'fragment') { + if (helpers.isOwner({ user, object })) { + return true + } + const userPermissions = await helpers.getUserPermissions({ user, Team: context.models.Team, - mapFn: t => ({ - objectId: t.object.id, - objectType: t.object.type, - permissions: t.teamType.permissions, - }), }) const permission = userPermissions.find( @@ -206,7 +137,8 @@ async function authenticatedUser(user, operation, object, context) { return { filter: fragment => { - if (permission.permissions === 'reviewer') { + // handle other roles + if (permission.role === 'reviewer') { fragment.files = omit(fragment.files, ['coverLetter']) fragment.authors = fragment.authors.map(a => omit(a, ['email'])) } @@ -214,140 +146,84 @@ async function authenticatedUser(user, operation, object, context) { }, } } - } - // TODO: in the future give him the non draft version of the fragment - if ( - operation === 'GET' && - get(object, 'type') === 'fragment' && - user.handlingEditor - ) { - return helpers.heIsInvitedToFragment({ - user, - Team: context.models.Team, - collectionId: object.collectionId, - }) - } - - if ( - operation === 'POST' && - object.path === '/collections/:collectionId/fragments' - ) { - return true - } - - // allow authenticate owners full pass for a collection - if (get(object, 'type') === 'collection') { - if (operation === 'PATCH') { - return { - filter: collection => omit(collection, 'filtered'), - } - } - if (object.owners.includes(user.id)) return true - const owner = object.owners.find(own => own.id === user.id) - if (owner !== undefined) return true - } - - // Allow owners of a collection to GET its teams, e.g. - // GET /api/collections/1/teams - if (operation === 'GET' && get(object, 'path') === '/teams') { - const collectionId = get(object, 'params.collectionId') - if (collectionId) { + // allow HE to get reviewer invitations + if (get(object, 'fragment.type') === 'fragment') { + const collectionId = get(object, 'fragment.collectionId') const collection = await context.models.Collection.find(collectionId) - if (collection.owners.includes(user.id)) { + + if (get(collection, 'handlingEditor.id') === user.id) { return true } } - } - if ( - operation === 'GET' && - get(object, 'type') === 'team' && - get(object, 'object.type') === 'collection' - ) { - const collection = await context.models.Collection.find( - get(object, 'object.id'), - ) - if (collection.owners.includes(user.id)) { + if (get(object, 'type') === 'user' && get(object, 'id') === user.id) { return true } } - // Advanced example - // Allow authenticated users to create a team based around a collection - // if they are one of the owners of this collection - if (['POST', 'PATCH'].includes(operation) && get(object, 'type') === 'team') { - if (get(object, 'object.type') === 'collection') { - const collection = await context.models.Collection.find( - get(object, 'object.id'), - ) - if (collection.owners.includes(user.id)) { - return true - } + if (operation === 'POST') { + // allow everytone to create manuscripts and versions + if (createPaths.includes(object.path)) { + return true } - } - - // only allow the HE to create, delete an invitation, or get invitation 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'), - ) - const handlingEditor = get(collection, 'handlingEditor') - if (!handlingEditor) return false - if (handlingEditor.id === user.id) return true - return false - } - - // only allow a reviewer and an HE to submit and to modify a recommendation - if ( - ['POST', 'PATCH'].includes(operation) && - object.path.includes('recommendations') - ) { - const authsomeObject = get(object, 'authsomeObject') - - const teams = await helpers.getTeamsByPermissions( - user.teams, - ['reviewer', 'handlingEditor'], - context.models.Team, - ) - if (teams.length === 0) return false - const matchingTeam = teams.find( - team => team.object.id === authsomeObject.id, - ) + // allow HE to invite + if ( + get(object, 'path') === + '/api/collections/:collectionId/fragments/:fragmentId/invitations' + ) { + return helpers.isHandlingEditor({ user, object }) + } - if (matchingTeam) return true - return false + // allow HE or assigned reviewers to recommend + if ( + get(object, 'path') === + '/api/collections/:collectionId/fragments/:fragmentId/recommendations' + ) { + return helpers.hasPermissionForObject({ + user, + object, + Team: context.models.Team, + }) + } } - if (user.teams.length !== 0 && ['GET'].includes(operation)) { - const permissions = await teamPermissions(user, operation, object, context) - - if (permissions) { - return permissions + if (operation === 'PATCH') { + if (get(object, 'type') === 'collection') { + return helpers.isOwner({ user, object }) } - return false - } + if (get(object, 'type') === 'fragment') { + return helpers.isOwner({ user, object }) + } - if (get(object, 'type') === 'fragment') { - const fragment = object + // allow reviewer to patch his recommendation + if ( + get(object, 'path') === + '/api/collections/:collectionId/fragments/:fragmentId/recommendations' + ) { + return helpers.hasPermissionForObject({ + user, + object, + Team: context.models.Team, + }) + } - if (fragment.owners.includes(user.id)) { + if (get(object, 'type') === 'user' && get(object, 'id') === user.id) { return true } } - // A user can GET, DELETE and PATCH itself - if (get(object, 'type') === 'user' && get(object, 'id') === user.id) { - if (['GET', 'DELETE', 'PATCH'].includes(operation)) { - return true + if (operation === 'DELETE') { + if ( + get(object, 'path') === + '/api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId' + ) { + return helpers.isHandlingEditor({ user, object }) } } + // If no individual permissions exist (above), fallback to unauthenticated // user's permission return unauthenticatedUser(operation, object) -- GitLab