Skip to content
Snippets Groups Projects
Commit 82c486b3 authored by Sebastian Mihalache's avatar Sebastian Mihalache
Browse files

feat(component-invite): add authsome to post route

parent 05474200
No related branches found
No related tags found
1 merge request!8Sprint #10
const omit = require('lodash/omit')
const config = require('config')
const get = require('lodash/get')
const statuses = config.get('statuses')
const publicStatusesPermissions = ['author', 'reviewer']
module.exports = {
parseReviewerAuthors: (coll, matchingCollPerm) => {
if (['reviewer'].includes(matchingCollPerm.permission)) {
coll.authors = coll.authors.map(a => omit(a, ['email']))
}
},
setPublicStatuses: (coll, matchingCollPerm) => {
const status = get(coll, 'status') || 'draft'
coll.visibleStatus = statuses[status].public
if (!publicStatusesPermissions.includes(matchingCollPerm.permission)) {
coll.visibleStatus = statuses[coll.status].private
}
},
filterRefusedInvitations: (coll, user) => {
const matchingInv = coll.invitations.find(inv => inv.userId === user.id)
if (matchingInv === undefined) return null
if (matchingInv.hasAnswer === true && !matchingInv.isAccepted) return null
return coll
},
}
const get = require('lodash/get')
const pickBy = require('lodash/pickBy')
const omit = require('lodash/omit')
const helpers = require('./authsome-helpers')
async function teamPermissions(user, operation, object, context) {
const permissions = ['handlingEditor', 'author', 'reviewer']
const teams = await Promise.all(
user.teams.map(async teamId => {
const team = await context.models.Team.find(teamId)
if (permissions.includes(team.teamType.permissions)) {
return team
}
return null
}),
)
const collectionsPermissions = teams.filter(Boolean).map(team => ({
id: team.object.id,
permission: team.teamType.permissions,
}))
if (collectionsPermissions.length > 0) {
return {
filter: filterParam => {
if (!filterParam.length) return filterParam
const collections = filterParam
.map(coll => {
const matchingCollPerm = collectionsPermissions.find(
collPerm => coll.id === collPerm.id,
)
if (matchingCollPerm === undefined) {
return null
}
helpers.setPublicStatuses(coll, matchingCollPerm)
helpers.parseReviewerAuthors(coll, matchingCollPerm)
if (
['reviewer', 'handlingEditor'].includes(
matchingCollPerm.permission,
)
) {
return helpers.filterRefusedInvitations(coll, user)
}
return coll
})
.filter(Boolean)
return collections
},
}
}
return {}
}
function unauthenticatedUser(operation, object) {
// Public/unauthenticated users can GET /collections, filtered by 'published'
if (operation === 'GET' && object && object.path === '/collections') {
return {
filter: collections =>
collections.filter(collection => collection.published),
}
}
// Public/unauthenticated users can GET /collections/:id/fragments, filtered by 'published'
if (
operation === 'GET' &&
object &&
object.path === '/collections/:id/fragments'
) {
return {
filter: fragments => fragments.filter(fragment => fragment.published),
}
}
// and filtered individual collection's properties: id, title, source, content, owners
if (operation === 'GET' && object && object.type === 'collection') {
if (object.published) {
return {
filter: collection =>
pickBy(collection, (_, key) =>
['id', 'title', 'owners'].includes(key),
),
}
}
}
if (operation === 'GET' && object && object.type === 'fragment') {
if (object.published) {
return {
filter: fragment =>
pickBy(fragment, (_, key) =>
['id', 'title', 'source', 'presentation', 'owners'].includes(key),
),
}
}
}
return false
}
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'),
}
}
// Allow the authenticated user to GET collections they own
if (operation === 'GET' && object === '/collections/') {
return {
filter: collection => collection.owners.includes(user.id),
}
}
// 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) {
const collection = await context.models.Collection.find(collectionId)
if (collection.owners.includes(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)) {
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
}
}
}
// 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'))
if (collection.handlingEditor.id === user.id) {
return true
}
return false
}
if (user.teams.length !== 0) {
const permissions = await teamPermissions(user, operation, object, context)
if (permissions) {
return permissions
}
}
if (get(object, 'type') === 'fragment') {
const fragment = object
if (fragment.owners.includes(user.id)) {
return true
}
}
if (get(object, 'type') === 'collection') {
if (['GET', 'DELETE'].includes(operation)) {
return true
}
// Only allow filtered updating (mirroring filtered creation) for non-admin users)
if (operation === 'PATCH') {
return {
filter: collection => omit(collection, 'filtered'),
}
}
}
// 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 no individual permissions exist (above), fallback to unauthenticated
// user's permission
return unauthenticatedUser(operation, object)
}
const authsomeMode = async (userId, operation, object, context) => {
if (!userId) {
return unauthenticatedUser(operation, object)
}
// It's up to us to retrieve the relevant models for our
// authorization/authsome mode, e.g.
const user = await context.models.User.find(userId)
// Admins and editor in chiefs can do anything
if (user && (user.admin === true || user.editorInChief === true)) return true
if (user) {
return authenticatedUser(user, operation, object, context)
}
return false
}
module.exports = authsomeMode
const path = require('path')
module.exports = {
authsome: {
mode: path.resolve(__dirname, 'authsome-mode.js'),
teams: {
handlingEditor: {
name: 'Handling Editors',
},
reviewer: {
name: 'Reviewer',
},
},
},
mailer: {
from: 'test@example.com',
},
......
const path = require('path')
module.exports = {
authsome: {
mode: path.resolve(__dirname, 'authsome-mode.js'),
teams: {
handlingEditor: {
name: 'Handling Editors',
},
reviewer: {
name: 'Reviewer',
},
},
},
mailer: {
from: 'test@example.com',
},
......
......@@ -3,6 +3,27 @@ const Authsome = require('authsome')
const mode = require(config.get('authsome.mode'))
const authsome = new Authsome({ ...config.authsome, mode }, {})
const getAuthsome = models =>
new Authsome(
{ ...config.authsome, mode },
{
// restrict methods passed to mode since these have to be shimmed on client
// any changes here should be reflected in the `withAuthsome` component of `pubsweet-client`
models: {
Collection: {
find: id => models.Collection.find(id),
},
Fragment: {
find: id => models.Fragment.find(id),
},
User: {
find: id => models.User.find(id),
},
Team: {
find: id => models.Team.find(id),
},
},
},
)
module.exports = authsome
module.exports = { getAuthsome }
......@@ -7,6 +7,7 @@ const config = require('config')
const mailService = require('pubsweet-component-mail-service')
const userHelper = require('../../helpers/User')
const invitationHelper = require('../../helpers/Invitation')
const authsomeHelper = require('../../helpers/authsome')
const configRoles = config.get('roles')
......@@ -25,6 +26,8 @@ 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`)
return res.status(400).json({ error: 'Cannot invite yourself' })
......@@ -41,6 +44,11 @@ module.exports = models => async (req, res) => {
})
}
const canPost = await authsome.can(req.user, 'POST', collection)
if (!canPost)
return res.status(403).json({
error: 'Unauthorized.',
})
const baseUrl = `${req.protocol}://${req.get('host')}`
const params = {
baseUrl,
......
......@@ -156,4 +156,20 @@ describe('Post collections invitations route handler', () => {
`User has already replied to a previous invitation.`,
)
})
it('should return an error when the user does not have invitation rights', async () => {
const { author } = testFixtures.users
const { collection } = testFixtures.collections
const req = httpMocks.createRequest({
body,
})
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())
expect(data.error).toEqual('Unauthorized.')
})
})
......@@ -153,6 +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'))
if (collection.handlingEditor.id === user.id) {
return true
}
return false
}
if (user.teams.length !== 0) {
const permissions = await teamPermissions(user, operation, object, context)
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment