Ocelot-Social/backend/src/middleware/permissionsMiddleware.ts
2023-07-12 11:53:49 +02:00

480 lines
15 KiB
TypeScript

import { rule, shield, deny, allow, or, and } from 'graphql-shield'
import { getNeode } from '../db/neo4j'
import CONFIG from '../config'
import { validateInviteCode } from '../schema/resolvers/transactions/inviteCodes'
const debug = !!CONFIG.DEBUG
const allowExternalErrors = true
const neode = getNeode()
const isAuthenticated = rule({
cache: 'contextual',
})(async (_parent, _args, ctx, _info) => {
return !!(ctx && ctx.user && ctx.user.id)
})
const isModerator = rule()(async (parent, args, { user }, info) => {
return user && (user.role === 'moderator' || user.role === 'admin')
})
const isAdmin = rule()(async (parent, args, { user }, info) => {
return user && user.role === 'admin'
})
const onlyYourself = rule({
cache: 'no_cache',
})(async (parent, args, context, info) => {
return context.user.id === args.id
})
const isMyOwn = rule({
cache: 'no_cache',
})(async (parent, args, { user }, info) => {
return user && user.id === parent.id
})
const isMySocialMedia = rule({
cache: 'no_cache',
})(async (_, args, { user }) => {
// We need a User
if (!user) {
return false
}
let socialMedia = await neode.find('SocialMedia', args.id)
// Did we find a social media node?
if (!socialMedia) {
return false
}
socialMedia = await socialMedia.toJson() // whats this for?
// Is it my social media entry?
return socialMedia.ownedBy.node.id === user.id
})
const isAllowedToChangeGroupSettings = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
if (!(user && user.id)) return false
const ownerId = user.id
const { id: groupId } = args
const session = driver.session()
const readTxPromise = session.readTransaction(async (transaction) => {
const transactionResponse = await transaction.run(
`
MATCH (owner:User {id: $ownerId})-[membership:MEMBER_OF]->(group:Group {id: $groupId})
RETURN group {.*}, owner {.*, myRoleInGroup: membership.role}
`,
{ groupId, ownerId },
)
return {
owner: transactionResponse.records.map((record) => record.get('owner'))[0],
group: transactionResponse.records.map((record) => record.get('group'))[0],
}
})
try {
const { owner, group } = await readTxPromise
return !!group && !!owner && ['owner'].includes(owner.myRoleInGroup)
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
})
const isAllowedSeeingGroupMembers = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
if (!(user && user.id)) return false
const { id: groupId } = args
const session = driver.session()
const readTxPromise = session.readTransaction(async (transaction) => {
const transactionResponse = await transaction.run(
`
MATCH (group:Group {id: $groupId})
OPTIONAL MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group)
RETURN group {.*}, member {.*, myRoleInGroup: membership.role}
`,
{ groupId, userId: user.id },
)
return {
member: transactionResponse.records.map((record) => record.get('member'))[0],
group: transactionResponse.records.map((record) => record.get('group'))[0],
}
})
try {
const { member, group } = await readTxPromise
return (
!!group &&
(group.groupType === 'public' ||
(['closed', 'hidden'].includes(group.groupType) &&
!!member &&
['usual', 'admin', 'owner'].includes(member.myRoleInGroup)))
)
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
})
const isAllowedToChangeGroupMemberRole = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
if (!(user && user.id)) return false
const currentUserId = user.id
const { groupId, userId, roleInGroup } = args
if (currentUserId === userId) return false
const session = driver.session()
const readTxPromise = session.readTransaction(async (transaction) => {
const transactionResponse = await transaction.run(
`
MATCH (currentUser:User {id: $currentUserId})-[currentUserMembership:MEMBER_OF]->(group:Group {id: $groupId})
OPTIONAL MATCH (group)<-[userMembership:MEMBER_OF]-(member:User {id: $userId})
RETURN group {.*}, currentUser {.*, myRoleInGroup: currentUserMembership.role}, member {.*, myRoleInGroup: userMembership.role}
`,
{ groupId, currentUserId, userId },
)
return {
currentUser: transactionResponse.records.map((record) => record.get('currentUser'))[0],
group: transactionResponse.records.map((record) => record.get('group'))[0],
member: transactionResponse.records.map((record) => record.get('member'))[0],
}
})
try {
const { currentUser, group, member } = await readTxPromise
const groupExists = !!group
const currentUserExists = !!currentUser
const userIsMember = !!member
const sameUserRoleInGroup = member && member.myRoleInGroup === roleInGroup
const userIsOwner = member && ['owner'].includes(member.myRoleInGroup)
const currentUserIsAdmin = currentUser && ['admin'].includes(currentUser.myRoleInGroup)
const adminCanSetRole = ['pending', 'usual', 'admin'].includes(roleInGroup)
const currentUserIsOwner = currentUser && ['owner'].includes(currentUser.myRoleInGroup)
const ownerCanSetRole = ['pending', 'usual', 'admin', 'owner'].includes(roleInGroup)
return (
groupExists &&
currentUserExists &&
(!userIsMember || (userIsMember && (sameUserRoleInGroup || !userIsOwner))) &&
((currentUserIsAdmin && adminCanSetRole) || (currentUserIsOwner && ownerCanSetRole))
)
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
})
const isAllowedToJoinGroup = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
if (!(user && user.id)) return false
const { groupId, userId } = args
const session = driver.session()
const readTxPromise = session.readTransaction(async (transaction) => {
const transactionResponse = await transaction.run(
`
MATCH (group:Group {id: $groupId})
OPTIONAL MATCH (group)<-[membership:MEMBER_OF]-(member:User {id: $userId})
RETURN group {.*}, member {.*, myRoleInGroup: membership.role}
`,
{ groupId, userId },
)
return {
group: transactionResponse.records.map((record) => record.get('group'))[0],
member: transactionResponse.records.map((record) => record.get('member'))[0],
}
})
try {
const { group, member } = await readTxPromise
return !!group && (group.groupType !== 'hidden' || (!!member && !!member.myRoleInGroup))
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
})
const isAllowedToLeaveGroup = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
if (!(user && user.id)) return false
const { groupId, userId } = args
if (user.id !== userId) return false
const session = driver.session()
const readTxPromise = session.readTransaction(async (transaction) => {
const transactionResponse = await transaction.run(
`
MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId})
RETURN group {.*}, member {.*, myRoleInGroup: membership.role}
`,
{ groupId, userId },
)
return {
group: transactionResponse.records.map((record) => record.get('group'))[0],
member: transactionResponse.records.map((record) => record.get('member'))[0],
}
})
try {
const { group, member } = await readTxPromise
return !!group && !!member && !!member.myRoleInGroup && member.myRoleInGroup !== 'owner'
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
})
const isMemberOfGroup = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
if (!(user && user.id)) return false
const { groupId } = args
if (!groupId) return true
const userId = user.id
const session = driver.session()
const readTxPromise = session.readTransaction(async (transaction) => {
const transactionResponse = await transaction.run(
`
MATCH (User {id: $userId})-[membership:MEMBER_OF]->(Group {id: $groupId})
RETURN membership.role AS role
`,
{ groupId, userId },
)
return transactionResponse.records.map((record) => record.get('role'))[0]
})
try {
const role = await readTxPromise
return ['usual', 'admin', 'owner'].includes(role)
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
})
const canRemoveUserFromGroup = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
if (!(user && user.id)) return false
const { groupId, userId } = args
const currentUserId = user.id
if (currentUserId === userId) return false
const session = driver.session()
const readTxPromise = session.readTransaction(async (transaction) => {
const transactionResponse = await transaction.run(
`
MATCH (User {id: $currentUserId})-[currentUserMembership:MEMBER_OF]->(group:Group {id: $groupId})
OPTIONAL MATCH (group)<-[userMembership:MEMBER_OF]-(user:User { id: $userId })
RETURN currentUserMembership.role AS currentUserRole, userMembership.role AS userRole
`,
{ currentUserId, groupId, userId },
)
return {
currentUserRole: transactionResponse.records.map((record) =>
record.get('currentUserRole'),
)[0],
userRole: transactionResponse.records.map((record) => record.get('userRole'))[0],
}
})
try {
const { currentUserRole, userRole } = await readTxPromise
return (
currentUserRole && ['owner'].includes(currentUserRole) && userRole && userRole !== 'owner'
)
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
})
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 }) => {
if (!user) return false
const { id: resourceId } = args
const session = driver.session()
const authorReadTxPromise = session.readTransaction(async (transaction) => {
const authorTransactionResponse = await transaction.run(
`
MATCH (resource {id: $resourceId})<-[:WROTE]-(author {id: $userId})
RETURN author
`,
{ resourceId, userId: user.id },
)
return authorTransactionResponse.records.map((record) => record.get('author'))
})
try {
const [author] = await authorReadTxPromise
return !!author
} finally {
session.close()
}
})
const isDeletingOwnAccount = rule({
cache: 'no_cache',
})(async (parent, args, context, _info) => {
return context.user.id === args.id
})
const noEmailFilter = rule({
cache: 'no_cache',
})(async (_, args) => {
return !('email' in args)
})
const publicRegistration = rule()(() => CONFIG.PUBLIC_REGISTRATION)
const inviteRegistration = rule()(async (_parent, args, { user, driver }) => {
if (!CONFIG.INVITE_REGISTRATION) return false
const { inviteCode } = args
const session = driver.session()
return validateInviteCode(session, inviteCode)
})
// Permissions
export default shield(
{
Query: {
'*': deny,
searchResults: allow,
searchPosts: allow,
searchUsers: allow,
searchGroups: allow,
searchHashtags: allow,
embed: allow,
Category: allow,
Tag: allow,
reports: isModerator,
statistics: allow,
currentUser: allow,
Group: isAuthenticated,
GroupMembers: isAllowedSeeingGroupMembers,
GroupCount: isAuthenticated,
Post: allow,
profilePagePosts: allow,
Comment: allow,
User: or(noEmailFilter, isAdmin),
isLoggedIn: allow,
Badge: allow,
PostsEmotionsCountByEmotion: allow,
PostsEmotionsByCurrentUser: isAuthenticated,
mutedUsers: isAuthenticated,
blockedUsers: isAuthenticated,
notifications: isAuthenticated,
Donations: isAuthenticated,
userData: isAuthenticated,
MyInviteCodes: isAuthenticated,
isValidInviteCode: allow,
VerifyNonce: allow,
queryLocations: isAuthenticated,
availableRoles: isAdmin,
getInviteCode: isAuthenticated, // and inviteRegistration
Room: isAuthenticated,
Message: isAuthenticated,
},
Mutation: {
'*': deny,
login: allow,
Signup: or(publicRegistration, inviteRegistration, isAdmin),
SignupVerification: allow,
UpdateUser: onlyYourself,
CreateGroup: isAuthenticated,
UpdateGroup: isAllowedToChangeGroupSettings,
JoinGroup: isAllowedToJoinGroup,
LeaveGroup: isAllowedToLeaveGroup,
ChangeGroupMemberRole: isAllowedToChangeGroupMemberRole,
RemoveUserFromGroup: canRemoveUserFromGroup,
CreatePost: and(isAuthenticated, isMemberOfGroup),
UpdatePost: isAuthor,
DeletePost: isAuthor,
fileReport: isAuthenticated,
CreateSocialMedia: isAuthenticated,
UpdateSocialMedia: isMySocialMedia,
DeleteSocialMedia: isMySocialMedia,
// AddBadgeRewarded: isAdmin,
// RemoveBadgeRewarded: isAdmin,
reward: isAdmin,
unreward: isAdmin,
followUser: isAuthenticated,
unfollowUser: isAuthenticated,
shout: isAuthenticated,
unshout: isAuthenticated,
changePassword: isAuthenticated,
review: isModerator,
CreateComment: and(isAuthenticated, canCommentPost),
UpdateComment: isAuthor,
DeleteComment: isAuthor,
DeleteUser: or(isDeletingOwnAccount, isAdmin),
requestPasswordReset: allow,
resetPassword: allow,
AddPostEmotions: isAuthenticated,
RemovePostEmotions: isAuthenticated,
muteUser: isAuthenticated,
unmuteUser: isAuthenticated,
blockUser: isAuthenticated,
unblockUser: isAuthenticated,
markAsRead: isAuthenticated,
markAllAsRead: isAuthenticated,
AddEmailAddress: isAuthenticated,
VerifyEmailAddress: isAuthenticated,
pinPost: isAdmin,
unpinPost: isAdmin,
UpdateDonations: isAdmin,
GenerateInviteCode: isAuthenticated,
switchUserRole: isAdmin,
markTeaserAsViewed: allow,
saveCategorySettings: isAuthenticated,
CreateRoom: isAuthenticated,
CreateMessage: isAuthenticated,
MarkMessagesAsSeen: isAuthenticated,
},
User: {
email: or(isMyOwn, isAdmin),
},
Report: isModerator,
},
{
debug,
allowExternalErrors,
fallbackRule: allow,
fallbackError: Error('Not Authorized!'),
},
)