fix(server): prevent creating project contributor invite to viewer se… (#4462)

* fix(server): prevent creating project contributor invite to viewer seat member

* undo regionConfig change

* moar cleanup
This commit is contained in:
Kristaps Fabians Geikins
2025-04-17 07:00:33 +03:00
committed by GitHub
parent 385157ac81
commit 93bc55630b
7 changed files with 227 additions and 103 deletions
@@ -57,7 +57,8 @@ import { WorkspaceInviteResourceType } from '@/modules/workspacesCore/domain/con
import {
GetWorkspace,
GetWorkspaceBySlug,
GetWorkspaceDomains
GetWorkspaceDomains,
ValidateWorkspaceMemberProjectRole
} from '@/modules/workspaces/domain/operations'
import { WorkspaceInviteResourceTarget } from '@/modules/workspaces/domain/types'
import { mapGqlWorkspaceRoleToMainRole } from '@/modules/workspaces/helpers/roles'
@@ -72,6 +73,7 @@ import {
} from '@/modules/workspaces/domain/logic'
import { GetStream } from '@/modules/core/domain/streams/operations'
import { GetUser } from '@/modules/core/domain/users/operations'
import { GetWorkspaceRoleAndSeat } from '@/modules/workspacesCore/domain/operations'
export const isWorkspaceResourceTarget = (
target: InviteResourceTarget
@@ -122,6 +124,8 @@ export const createWorkspaceInviteFactory =
type CollectAndValidateWorkspaceTargetsFactoryDeps =
CollectAndValidateCoreTargetsFactoryDeps & {
getWorkspace: GetWorkspace
getWorkspaceRoleAndSeat: GetWorkspaceRoleAndSeat
validateWorkspaceMemberProjectRoleFactory: ValidateWorkspaceMemberProjectRole
getWorkspaceDomains: GetWorkspaceDomains
findVerifiedEmailsByUserId: FindVerifiedEmailsByUserId
getStream: GetStream
@@ -191,31 +195,42 @@ export const collectAndValidateWorkspaceTargetsFactory =
return [...baseTargets]
}
const workspace = await deps.getWorkspace({
workspaceId,
userId: targetUser?.id
})
const [workspace, workspaceRoleAndSeat] = await Promise.all([
deps.getWorkspace({
workspaceId
}),
...(targetUser?.id
? [
deps.getWorkspaceRoleAndSeat({
workspaceId,
userId: targetUser.id
})
]
: [])
])
if (!workspace) {
throw new InviteCreateValidationError(
'Attempting to invite into a non-existant workspace'
)
}
// If inviting to workspace project, disallow workspace guests to become project owners
const workspaceRole = workspaceRoleAndSeat?.role.role
// If inviting to workspace project, validate target role
const projectTarget = baseTargets.find(isProjectResourceTarget)
if (
workspace?.role === Roles.Workspace.Guest &&
projectTarget?.role === Roles.Stream.Owner
) {
throw new InviteCreateValidationError(
'Workspace guests cannot be owners of workspace projects'
)
const projectRole = projectTarget?.role
if (projectRole && targetUser) {
await deps.validateWorkspaceMemberProjectRoleFactory({
workspaceId,
userId: targetUser.id,
projectRole
})
}
// Do further validation only if we're actually planning to invite to a workspace
// (maybe the invitation is implicitly there, but user already is a member of the workspace)
const isInvitingToWorkspace =
primaryWorkspaceResourceTarget || (workspace && !workspace.role)
primaryWorkspaceResourceTarget || (workspace && !workspaceRole)
if (!isInvitingToWorkspace) {
return [...baseTargets]
}
@@ -236,7 +251,7 @@ export const collectAndValidateWorkspaceTargetsFactory =
}
// Only check this on creation, on finalization its fine if the user's already a member
if (workspace.role && !finalizingInvite) {
if (workspaceRole && !finalizingInvite) {
throw new InviteCreateValidationError(
'The target user is already a member of the specified workspace'
)
@@ -6,16 +6,18 @@ import {
GetWorkspaceSeatTypeToProjectRoleMapping,
IntersectProjectCollaboratorsAndWorkspaceCollaborators,
QueryAllWorkspaceProjects,
UpdateWorkspaceRole
UpdateWorkspaceRole,
ValidateWorkspaceMemberProjectRole
} from '@/modules/workspaces/domain/operations'
import {
WorkspaceInvalidProjectError,
WorkspaceInvalidRoleError,
WorkspaceNotFoundError,
WorkspaceQueryError
} from '@/modules/workspaces/errors/workspace'
import { GetProject, UpdateProject } from '@/modules/core/domain/projects/operations'
import { chunk } from 'lodash'
import { Roles } from '@speckle/shared'
import { Roles, StreamRoles } from '@speckle/shared'
import {
GetStreamCollaborators,
LegacyGetStreams,
@@ -42,6 +44,7 @@ import {
upsertWorkspaceFactory
} from '@/modules/workspaces/repositories/workspaces'
import {
GetWorkspaceRoleAndSeat,
GetWorkspaceRolesAndSeats,
GetWorkspaceWithPlan,
WorkspaceSeatType
@@ -296,6 +299,69 @@ export const getWorkspaceSeatTypeToProjectRoleMappingFactory =
}
}
/**
* Validate that the specified workspace member can have the specified project role
*/
export const validateWorkspaceMemberProjectRoleFactory =
(deps: {
getWorkspaceRoleAndSeat: GetWorkspaceRoleAndSeat
getWorkspaceWithPlan: GetWorkspaceWithPlan
getWorkspaceRoleToDefaultProjectRoleMapping: GetWorkspaceRoleToDefaultProjectRoleMapping
getWorkspaceSeatTypeToProjectRoleMapping: GetWorkspaceSeatTypeToProjectRoleMapping
}): ValidateWorkspaceMemberProjectRole =>
async (params) => {
const { workspaceId, userId, projectRole } = params
const roleSeatParams = {
workspaceId,
userId
}
const [currentWorkspaceRoleAndSeat, workspace] = await Promise.all([
deps.getWorkspaceRoleAndSeat(roleSeatParams),
deps.getWorkspaceWithPlan({ workspaceId })
])
if (!workspace || !currentWorkspaceRoleAndSeat?.role) return
const {
role: { role: workspaceRole },
seat
} = currentWorkspaceRoleAndSeat
const seatType = seat?.type || WorkspaceSeatType.Viewer
let allowedRoles: StreamRoles[]
const isNewPlan = workspace.plan && isNewPlanType(workspace.plan.name)
if (isNewPlan) {
const workspaceAllowedRoles = (
await deps.getWorkspaceRoleToDefaultProjectRoleMapping({
workspaceId
})
).allowed[workspaceRole]
const seatAllowedRoles = (
await deps.getWorkspaceSeatTypeToProjectRoleMapping({
workspaceId
})
).allowed[seatType]
allowedRoles = Array.from(
new Set(workspaceAllowedRoles).intersection(new Set(seatAllowedRoles))
)
} else {
const roleMapping = await deps.getWorkspaceRoleToDefaultProjectRoleMapping({
workspaceId
})
allowedRoles = roleMapping.allowed[workspaceRole]
}
if (!allowedRoles.includes(projectRole)) {
// User's workspace role does not allow the requested project role
throw new WorkspaceInvalidRoleError(
isNewPlan
? `User's workspace seat type '${seatType}' does not allow project role '${projectRole}'.`
: `User's workspace role '${workspaceRole}' does not allow project role '${projectRole}'.`
)
}
}
export const createWorkspaceProjectFactory =
(deps: { getDefaultRegion: GetDefaultRegion }) =>
async (params: { input: WorkspaceProjectCreateInput; ownerId: string }) => {