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