diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 728a248fb..906285d12 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -249,6 +249,40 @@ const isMemberOfGroup = rule({ } }) +const canCommentPost = rule({ + cache: 'no_cache', +})(async (_parent, args, { user, driver }) => { + if (!(user && user.id)) return false + const { postId } = args + const userId = user.id + const session = driver.session() + const readTxPromise = session.readTransaction(async (transaction) => { + const transactionResponse = await transaction.run( + ` + MATCH (post:Post { id: $postId }) + OPTIONAL MATCH (post)-[:IN]->(group:Group) + OPTIONAL MATCH (user:User { id: $userId })-[membership:MEMBER_OF]->(group) + RETURN group AS group, membership AS membership + `, + { postId, userId }, + ) + return { + group: transactionResponse.records.map((record) => record.get('group'))[0], + membership: transactionResponse.records.map((record) => record.get('membership'))[0], + } + }) + try { + const { group, membership } = await readTxPromise + return ( + !group || (membership && ['usual', 'admin', 'owner'].includes(membership.properties.role)) + ) + } catch (error) { + throw new Error(error) + } finally { + session.close() + } +}) + const isAuthor = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { @@ -361,7 +395,7 @@ export default shield( unshout: isAuthenticated, changePassword: isAuthenticated, review: isModerator, - CreateComment: isAuthenticated, + CreateComment: and(isAuthenticated, canCommentPost), UpdateComment: isAuthor, DeleteComment: isAuthor, DeleteUser: or(isDeletingOwnAccount, isAdmin), diff --git a/backend/src/schema/resolvers/postsInGroups.spec.js b/backend/src/schema/resolvers/postsInGroups.spec.js index d17c928ec..2c1e88b62 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.js +++ b/backend/src/schema/resolvers/postsInGroups.spec.js @@ -14,6 +14,7 @@ import { profilePagePosts, searchPosts, } from '../../db/graphql/posts' +import { createCommentMutation } from '../../db/graphql/comments' // eslint-disable-next-line no-unused-vars import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '../../constants/groups' import CONFIG from '../../config' @@ -378,6 +379,170 @@ describe('Posts in Groups', () => { }) }) + describe('commenting posts in groups', () => { + describe('without membership of group', () => { + beforeAll(async () => { + authenticatedUser = await anyUser.toJson() + }) + + it('throws an error for public groups', async () => { + await expect( + mutate({ + mutation: createCommentMutation, + variables: { + postId: 'post-to-public-group', + content: + 'I am commenting a post in a public group without being a member of the group', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]), + }) + }) + + it('throws an error for closed groups', async () => { + await expect( + mutate({ + mutation: createCommentMutation, + variables: { + postId: 'post-to-closed-group', + content: + 'I am commenting a post in a closed group without being a member of the group', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]), + }) + }) + + it('throws an error for hidden groups', async () => { + await expect( + mutate({ + mutation: createCommentMutation, + variables: { + postId: 'post-to-hidden-group', + content: + 'I am commenting a post in a hidden group without being a member of the group', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]), + }) + }) + }) + + describe('as a pending member of group', () => { + beforeAll(async () => { + authenticatedUser = await pendingUser.toJson() + }) + + it('throws an error for public groups', async () => { + await expect( + mutate({ + mutation: createCommentMutation, + variables: { + postId: 'post-to-public-group', + content: 'I am commenting a post in a public group as a pending member of the group', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]), + }) + }) + + it('throws an error for closed groups', async () => { + await expect( + mutate({ + mutation: createCommentMutation, + variables: { + postId: 'post-to-closed-group', + content: 'I am commenting a post in a closed group as a pending member of the group', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]), + }) + }) + + it('throws an error for hidden groups', async () => { + await expect( + mutate({ + mutation: createCommentMutation, + variables: { + postId: 'post-to-hidden-group', + content: 'I am commenting a post in a hidden group as a pending member of the group', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]), + }) + }) + }) + + describe('as a member of group', () => { + beforeAll(async () => { + authenticatedUser = await allGroupsUser.toJson() + }) + + it('comments a post in a public group', async () => { + await expect( + mutate({ + mutation: createCommentMutation, + variables: { + postId: 'post-to-public-group', + content: 'I am commenting a post in a public group as a member of the group', + }, + }), + ).resolves.toMatchObject({ + data: { + CreateComment: { + id: expect.any(String), + }, + }, + errors: undefined, + }) + }) + + it('comments a post in a closed group', async () => { + await expect( + mutate({ + mutation: createCommentMutation, + variables: { + postId: 'post-to-closed-group', + content: 'I am commenting a post in a closed group as a member of the group', + }, + }), + ).resolves.toMatchObject({ + data: { + CreateComment: { + id: expect.any(String), + }, + }, + errors: undefined, + }) + }) + + it('comments a post in a hidden group', async () => { + await expect( + mutate({ + mutation: createCommentMutation, + variables: { + postId: 'post-to-hidden-group', + content: 'I am commenting a post in a hidden group as a member of the group', + }, + }), + ).resolves.toMatchObject({ + data: { + CreateComment: { + id: expect.any(String), + }, + }, + errors: undefined, + }) + }) + }) + }) + describe('visibility of posts', () => { describe('query post by ID', () => { describe('without authentication', () => {