Merge branch 'main' of github.com:specklesystems/speckle-server into alessandro/web-1767-guest-table-should-show-what-they-have-access-to

This commit is contained in:
Alessandro Magionami
2024-09-13 12:53:06 +02:00
28 changed files with 781 additions and 486 deletions
@@ -3,7 +3,11 @@ import {
ProjectEvents,
ProjectEventsPayloads
} from '@/modules/core/events/projectsEmitter'
import { getStream } from '@/modules/core/repositories/streams'
import {
deleteProjectRoleFactory,
getStream,
upsertProjectRoleFactory
} from '@/modules/core/repositories/streams'
import {
GetWorkspaceRoles,
GetWorkspaceRoleToDefaultProjectRoleMapping,
@@ -17,13 +21,27 @@ import {
isProjectResourceTarget,
resolveTarget
} from '@/modules/serverinvites/helpers/core'
import { logger } from '@/logging/logging'
import { logger, moduleLogger } from '@/logging/logging'
import { updateWorkspaceRoleFactory } from '@/modules/workspaces/services/management'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { WorkspaceInviteResourceType } from '@/modules/workspaces/domain/constants'
import { Roles, WorkspaceRoles } from '@speckle/shared'
import { UpsertProjectRole } from '@/modules/core/domain/projects/operations'
import {
DeleteProjectRole,
UpsertProjectRole
} from '@/modules/core/domain/projects/operations'
import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events'
import { Knex } from 'knex'
import { mapWorkspaceRoleToInitialProjectRole } from '@/modules/workspaces/domain/logic'
import {
getWorkspaceRolesFactory,
getWorkspaceWithDomainsFactory,
upsertWorkspaceRoleFactory
} from '@/modules/workspaces/repositories/workspaces'
import { queryAllWorkspaceProjectsFactory } from '@/modules/workspaces/services/projects'
import { getStreams } from '@/modules/core/services/streams'
import { withTransaction } from '@/modules/shared/helpers/dbHelper'
import { findVerifiedEmailsByUserIdFactory } from '@/modules/core/repositories/userEmails'
export const onProjectCreatedFactory =
({
@@ -89,7 +107,7 @@ export const onInviteFinalizedFactory =
})
if (!project || !project.role) {
deps.logger.warn(
`When handling accepted invite - project not found or useris not a collaborator`,
`When handling accepted invite - project not found or user is not a collaborator`,
{ invite, project: { id: project?.id, role: project?.role } }
)
return
@@ -109,39 +127,77 @@ export const onInviteFinalizedFactory =
})
}
export const onWorkspaceJoinedFactory =
export const onWorkspaceRoleDeletedFactory =
({
queryAllWorkspaceProjects,
deleteProjectRole
}: {
queryAllWorkspaceProjects: QueryAllWorkspaceProjects
deleteProjectRole: DeleteProjectRole
}) =>
async ({ userId, workspaceId }: { userId: string; workspaceId: string }) => {
// Delete roles for all workspace projects
for await (const projectsPage of queryAllWorkspaceProjects({
workspaceId
})) {
await Promise.all(
projectsPage.map(({ id: projectId }) =>
deleteProjectRole({ projectId, userId })
)
)
}
}
export const onWorkspaceRoleUpdatedFactory =
({
getDefaultWorkspaceProjectRoleMapping,
queryAllWorkspaceProjects,
deleteProjectRole,
upsertProjectRole
}: {
getDefaultWorkspaceProjectRoleMapping: GetWorkspaceRoleToDefaultProjectRoleMapping
queryAllWorkspaceProjects: QueryAllWorkspaceProjects
deleteProjectRole: DeleteProjectRole
upsertProjectRole: UpsertProjectRole
}) =>
async ({
userId,
role,
workspaceId
workspaceId,
flags
}: {
userId: string
role: WorkspaceRoles
workspaceId: string
flags?: {
skipProjectRoleUpdatesFor: string[]
}
}) => {
const defaultRoleMapping = await getDefaultWorkspaceProjectRoleMapping({
const defaultProjectRoleMapping = await getDefaultWorkspaceProjectRoleMapping({
workspaceId
})
const maybeProjectRole = defaultRoleMapping[role]
if (!maybeProjectRole) return
const nextProjectRole = defaultProjectRoleMapping[role]
for await (const projects of queryAllWorkspaceProjects({ workspaceId })) {
for await (const projectsPage of queryAllWorkspaceProjects({ workspaceId })) {
await Promise.all(
projects.map(async (project) => {
projectsPage.map(async ({ id: projectId }) => {
if (flags?.skipProjectRoleUpdatesFor.includes(projectId)) {
// Skip assignment (used during invite flow)
// TODO: Can we refactor this special case away?
return
}
if (!nextProjectRole) {
// User is being demoted to a workspace role without project access
await deleteProjectRole({ projectId, userId })
return
}
await upsertProjectRole({
projectId: project.id,
projectId,
userId,
role: maybeProjectRole
role: nextProjectRole
})
})
)
@@ -149,25 +205,50 @@ export const onWorkspaceJoinedFactory =
}
export const initializeEventListenersFactory =
({
onProjectCreated,
onInviteFinalized,
onWorkspaceJoined
}: {
onProjectCreated: ReturnType<typeof onProjectCreatedFactory>
onInviteFinalized: ReturnType<typeof onInviteFinalizedFactory>
onWorkspaceJoined: ReturnType<typeof onWorkspaceJoinedFactory>
}) =>
({ db }: { db: Knex }) =>
() => {
const eventBus = getEventBus()
const quitCbs = [
ProjectsEmitter.listen(ProjectEvents.Created, onProjectCreated),
eventBus.listen(ServerInvitesEvents.Finalized, ({ payload }) =>
onInviteFinalized(payload)
),
eventBus.listen(WorkspaceEvents.JoinedFromDiscovery, ({ payload }) =>
onWorkspaceJoined(payload)
)
ProjectsEmitter.listen(ProjectEvents.Created, async (payload) => {
const onProjectCreated = onProjectCreatedFactory({
getDefaultWorkspaceProjectRoleMapping: mapWorkspaceRoleToInitialProjectRole,
upsertProjectRole: upsertProjectRoleFactory({ db }),
getWorkspaceRoles: getWorkspaceRolesFactory({ db })
})
await onProjectCreated(payload)
}),
eventBus.listen(ServerInvitesEvents.Finalized, async ({ payload }) => {
const onInviteFinalized = onInviteFinalizedFactory({
getStream,
logger: moduleLogger,
updateWorkspaceRole: updateWorkspaceRoleFactory({
getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }),
findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db }),
getWorkspaceRoles: getWorkspaceRolesFactory({ db }),
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
emitWorkspaceEvent: (...args) => getEventBus().emit(...args)
})
})
await onInviteFinalized(payload)
}),
eventBus.listen(WorkspaceEvents.RoleDeleted, async ({ payload }) => {
const trx = await db.transaction()
const onWorkspaceRoleDeleted = onWorkspaceRoleDeletedFactory({
queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ getStreams }),
deleteProjectRole: deleteProjectRoleFactory({ db: trx })
})
await withTransaction(onWorkspaceRoleDeleted(payload), trx)
}),
eventBus.listen(WorkspaceEvents.RoleUpdated, async ({ payload }) => {
const trx = await db.transaction()
const onWorkspaceRoleUpdated = onWorkspaceRoleUpdatedFactory({
getDefaultWorkspaceProjectRoleMapping: mapWorkspaceRoleToInitialProjectRole,
queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ getStreams }),
deleteProjectRole: deleteProjectRoleFactory({ db: trx }),
upsertProjectRole: upsertProjectRoleFactory({ db: trx })
})
await withTransaction(onWorkspaceRoleUpdated(payload), trx)
})
]
return () => quitCbs.forEach((quit) => quit())
@@ -5,8 +5,6 @@ import {
getStream,
getUserStreams,
getUserStreamsCount,
upsertProjectRoleFactory,
deleteProjectRoleFactory,
getRolesByUserIdFactory
} from '@/modules/core/repositories/streams'
import { getUser, getUsers } from '@/modules/core/repositories/users'
@@ -123,7 +121,6 @@ import {
isUserWorkspaceDomainPolicyCompliantFactory
} from '@/modules/workspaces/services/domains'
import { getServerInfo } from '@/modules/core/services/generic'
import { mapWorkspaceRoleToInitialProjectRole } from '@/modules/workspaces/domain/logic'
import { updateStreamRoleAndNotify } from '@/modules/core/services/streams/management'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
@@ -355,10 +352,6 @@ export = FF_WORKSPACES_MODULE_ENABLED
const deleteWorkspaceRole = deleteWorkspaceRoleFactory({
deleteWorkspaceRole: repoDeleteWorkspaceRoleFactory({ db: trx }),
getWorkspaceRoles: getWorkspaceRolesFactory({ db: trx }),
deleteProjectRole: deleteProjectRoleFactory({ db: trx }),
queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({
getStreams
}),
emitWorkspaceEvent: getEventBus().emit
})
@@ -377,13 +370,6 @@ export = FF_WORKSPACES_MODULE_ENABLED
db: trx
}),
getWorkspaceRoles: getWorkspaceRolesFactory({ db: trx }),
getDefaultWorkspaceProjectRoleMapping:
mapWorkspaceRoleToInitialProjectRole,
upsertProjectRole: upsertProjectRoleFactory({ db: trx }),
deleteProjectRole: deleteProjectRoleFactory({ db: trx }),
queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({
getStreams
}),
emitWorkspaceEvent: getEventBus().emit
})
@@ -466,8 +452,6 @@ export = FF_WORKSPACES_MODULE_ENABLED
const deleteWorkspaceRole = deleteWorkspaceRoleFactory({
deleteWorkspaceRole: repoDeleteWorkspaceRoleFactory({ db: trx }),
getWorkspaceRoles: getWorkspaceRolesFactory({ db: trx }),
deleteProjectRole: deleteProjectRoleFactory({ db: trx }),
queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ getStreams }),
emitWorkspaceEvent: getEventBus().emit
})
@@ -581,13 +565,6 @@ export = FF_WORKSPACES_MODULE_ENABLED
findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db }),
getWorkspaceRoles: getWorkspaceRolesFactory({ db }),
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
upsertProjectRole: upsertProjectRoleFactory({ db }),
getDefaultWorkspaceProjectRoleMapping:
mapWorkspaceRoleToInitialProjectRole,
deleteProjectRole: deleteProjectRoleFactory({ db }),
queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({
getStreams
}),
emitWorkspaceEvent: getEventBus().emit
})
}),
+2 -49
View File
@@ -6,29 +6,8 @@ import { Optional, SpeckleModule } from '@/modules/shared/helpers/typeHelper'
import { workspaceRoles } from '@/modules/workspaces/roles'
import { workspaceScopes } from '@/modules/workspaces/scopes'
import { registerOrUpdateRole } from '@/modules/shared/repositories/roles'
import {
initializeEventListenersFactory,
onInviteFinalizedFactory,
onProjectCreatedFactory,
onWorkspaceJoinedFactory
} from '@/modules/workspaces/events/eventListener'
import {
getWorkspaceRolesFactory,
getWorkspaceWithDomainsFactory,
upsertWorkspaceRoleFactory
} from '@/modules/workspaces/repositories/workspaces'
import {
deleteProjectRoleFactory,
getStream,
upsertProjectRoleFactory
} from '@/modules/core/repositories/streams'
import { updateWorkspaceRoleFactory } from '@/modules/workspaces/services/management'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { getStreams } from '@/modules/core/services/streams'
import { findVerifiedEmailsByUserIdFactory } from '@/modules/core/repositories/userEmails'
import { initializeEventListenersFactory } from '@/modules/workspaces/events/eventListener'
import { validateModuleLicense } from '@/modules/gatekeeper/services/validateLicense'
import { queryAllWorkspaceProjectsFactory } from '@/modules/workspaces/services/projects'
import { mapWorkspaceRoleToInitialProjectRole } from '@/modules/workspaces/domain/logic'
const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
@@ -58,33 +37,7 @@ const workspacesModule: SpeckleModule = {
moduleLogger.info('⚒️ Init workspaces module')
if (isInitial) {
quitListeners = initializeEventListenersFactory({
onProjectCreated: onProjectCreatedFactory({
getDefaultWorkspaceProjectRoleMapping: mapWorkspaceRoleToInitialProjectRole,
upsertProjectRole: upsertProjectRoleFactory({ db }),
getWorkspaceRoles: getWorkspaceRolesFactory({ db })
}),
onWorkspaceJoined: onWorkspaceJoinedFactory({
getDefaultWorkspaceProjectRoleMapping: mapWorkspaceRoleToInitialProjectRole,
queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ getStreams }),
upsertProjectRole: upsertProjectRoleFactory({ db })
}),
onInviteFinalized: onInviteFinalizedFactory({
getStream,
logger: moduleLogger,
updateWorkspaceRole: updateWorkspaceRoleFactory({
getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }),
findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db }),
getWorkspaceRoles: getWorkspaceRolesFactory({ db }),
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
getDefaultWorkspaceProjectRoleMapping: mapWorkspaceRoleToInitialProjectRole,
upsertProjectRole: upsertProjectRoleFactory({ db }),
deleteProjectRole: deleteProjectRoleFactory({ db }),
queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ getStreams }),
emitWorkspaceEvent: (...args) => getEventBus().emit(...args)
})
})
})()
quitListeners = initializeEventListenersFactory({ db })()
}
await Promise.all([initScopes(), initRoles()])
},
@@ -9,7 +9,6 @@ import {
UpsertWorkspaceRole,
GetWorkspaceWithDomains,
GetWorkspaceDomains,
GetWorkspaceRoleToDefaultProjectRoleMapping,
UpdateWorkspace
} from '@/modules/workspaces/domain/operations'
import {
@@ -52,10 +51,6 @@ import { DeleteAllResourceInvites } from '@/modules/serverinvites/domain/operati
import { WorkspaceInviteResourceType } from '@/modules/workspaces/domain/constants'
import { ProjectInviteResourceType } from '@/modules/serverinvites/domain/constants'
import { chunk, isEmpty, omit } from 'lodash'
import {
DeleteProjectRole,
UpsertProjectRole
} from '@/modules/core/domain/projects/operations'
import { userEmailsCompliantWithWorkspaceDomains } from '@/modules/workspaces/domain/logic'
import { workspaceRoles as workspaceRoleDefinitions } from '@/modules/workspaces/roles'
import { blockedDomains } from '@speckle/shared'
@@ -229,15 +224,11 @@ export const deleteWorkspaceRoleFactory =
({
getWorkspaceRoles,
deleteWorkspaceRole,
emitWorkspaceEvent,
queryAllWorkspaceProjects,
deleteProjectRole
emitWorkspaceEvent
}: {
getWorkspaceRoles: GetWorkspaceRoles
deleteWorkspaceRole: DeleteWorkspaceRole
emitWorkspaceEvent: EmitWorkspaceEvent
queryAllWorkspaceProjects: QueryAllWorkspaceProjects
deleteProjectRole: DeleteProjectRole
}) =>
async ({
workspaceId,
@@ -255,17 +246,6 @@ export const deleteWorkspaceRoleFactory =
return null
}
// Delete workspace project roles
for await (const projectsPage of queryAllWorkspaceProjects({
workspaceId
})) {
await Promise.all(
projectsPage.map(({ id: projectId }) =>
deleteProjectRole({ projectId, userId })
)
)
}
// Emit deleted role
await emitWorkspaceEvent({
eventName: WorkspaceEvents.RoleDeleted,
@@ -295,20 +275,12 @@ export const updateWorkspaceRoleFactory =
getWorkspaceWithDomains,
findVerifiedEmailsByUserId,
upsertWorkspaceRole,
upsertProjectRole,
deleteProjectRole,
getDefaultWorkspaceProjectRoleMapping,
queryAllWorkspaceProjects,
emitWorkspaceEvent
}: {
getWorkspaceRoles: GetWorkspaceRoles
getWorkspaceWithDomains: GetWorkspaceWithDomains
findVerifiedEmailsByUserId: FindVerifiedEmailsByUserId
upsertWorkspaceRole: UpsertWorkspaceRole
upsertProjectRole: UpsertProjectRole
deleteProjectRole: DeleteProjectRole
getDefaultWorkspaceProjectRoleMapping: GetWorkspaceRoleToDefaultProjectRoleMapping
queryAllWorkspaceProjects: QueryAllWorkspaceProjects
emitWorkspaceEvent: EmitWorkspaceEvent
}) =>
async ({
@@ -327,8 +299,16 @@ export const updateWorkspaceRoleFactory =
*/
preventRoleDowngrade?: boolean
}): Promise<void> => {
// Protect against removing last admin
const workspaceRoles = await getWorkspaceRoles({ workspaceId })
// Return early if no work required
const previousWorkspaceRole = workspaceRoles.find((acl) => acl.userId === userId)
if (previousWorkspaceRole?.role === nextWorkspaceRole) {
return
}
// Protect against removing last admin
if (
isUserLastWorkspaceAdmin(workspaceRoles, userId) &&
nextWorkspaceRole !== Roles.Workspace.Admin
@@ -336,8 +316,6 @@ export const updateWorkspaceRoleFactory =
throw new WorkspaceAdminRequiredError()
}
const previousWorkspaceRole = workspaceRoles.find((acl) => acl.userId === userId)
// prevent role downgrades (used during invite flow)
if (preventRoleDowngrade) {
if (previousWorkspaceRole) {
@@ -369,7 +347,7 @@ export const updateWorkspaceRoleFactory =
}
}
// Perform upsert
// Perform and emit change
await upsertWorkspaceRole({
userId,
workspaceId,
@@ -377,45 +355,17 @@ export const updateWorkspaceRoleFactory =
createdAt: previousWorkspaceRole?.createdAt ?? new Date()
})
// Emit new role
await emitWorkspaceEvent({
eventName: WorkspaceEvents.RoleUpdated,
payload: { userId, workspaceId, role: nextWorkspaceRole }
payload: {
userId,
workspaceId,
role: nextWorkspaceRole,
flags: {
skipProjectRoleUpdatesFor: skipProjectRoleUpdatesFor ?? []
}
}
})
// Update roles for all workspace projects
const defaultProjectRoleMapping = await getDefaultWorkspaceProjectRoleMapping({
workspaceId
})
for await (const projectsPage of queryAllWorkspaceProjects({
workspaceId
})) {
await Promise.all(
projectsPage.map(({ id: projectId }) => {
// skip assigning project role implied by workspace role (used during invite flow)
if (skipProjectRoleUpdatesFor?.includes(projectId)) {
return
}
// no change required
if (previousWorkspaceRole?.role === nextWorkspaceRole) return
const nextProjectRole = defaultProjectRoleMapping[nextWorkspaceRole]
// user is being removed from workspace or demoted to workspace guest
if (!nextWorkspaceRole || !nextProjectRole)
return deleteProjectRole({ projectId, userId })
// user is being granted a workspace role with new role for given project
return upsertProjectRole({
projectId,
userId,
role: nextProjectRole
})
})
)
}
}
export const addDomainToWorkspaceFactory =
@@ -1,21 +1,15 @@
import { db } from '@/db/knex'
import {
deleteProjectRoleFactory,
getStream,
upsertProjectRoleFactory
} from '@/modules/core/repositories/streams'
import { getStream } from '@/modules/core/repositories/streams'
import {
findEmailsByUserIdFactory,
findVerifiedEmailsByUserIdFactory
} from '@/modules/core/repositories/userEmails'
import { getStreams } from '@/modules/core/services/streams'
import {
findUserByTargetFactory,
insertInviteAndDeleteOldFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { mapWorkspaceRoleToInitialProjectRole } from '@/modules/workspaces/domain/logic'
import {
getWorkspaceRolesFactory,
upsertWorkspaceFactory,
@@ -38,7 +32,6 @@ import {
updateWorkspaceFactory,
addDomainToWorkspaceFactory
} from '@/modules/workspaces/services/management'
import { queryAllWorkspaceProjectsFactory } from '@/modules/workspaces/services/projects'
import { BasicTestUser } from '@/test/authHelper'
import { CreateWorkspaceInviteMutationVariables } from '@/test/graphql/generated/graphql'
import { MaybeNullOrUndefined, Roles, WorkspaceRoles } from '@speckle/shared'
@@ -138,10 +131,6 @@ export const assignToWorkspace = async (
findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db }),
getWorkspaceRoles: getWorkspaceRolesFactory({ db }),
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
getDefaultWorkspaceProjectRoleMapping: mapWorkspaceRoleToInitialProjectRole,
upsertProjectRole: upsertProjectRoleFactory({ db }),
deleteProjectRole: deleteProjectRoleFactory({ db }),
queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ getStreams }),
emitWorkspaceEvent: (...args) => getEventBus().emit(...args)
})
@@ -159,8 +148,6 @@ export const unassignFromWorkspace = async (
const deleteWorkspaceRole = deleteWorkspaceRoleFactory({
getWorkspaceRoles: getWorkspaceRolesFactory({ db }),
deleteWorkspaceRole: dbDeleteWorkspaceRoleFactory({ db }),
deleteProjectRole: deleteProjectRoleFactory({ db }),
queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ getStreams }),
emitWorkspaceEvent: (...args) => getEventBus().emit(...args)
})
@@ -565,11 +565,9 @@ describe('Workspaces GQL CRUD', () => {
// first 10 users
await createTestUsers(freeGuests)
await Promise.all(
freeGuests.map((guest) =>
assignToWorkspace(workspace, guest, Roles.Workspace.Guest)
)
)
for (const guest of freeGuests) {
await assignToWorkspace(workspace, guest, Roles.Workspace.Guest)
}
await Promise.all([
createTestUser(member),
@@ -4,7 +4,7 @@ import { Roles, StreamRoles } from '@speckle/shared'
import { StreamAclRecord, StreamRecord } from '@/modules/core/helpers/types'
import {
onProjectCreatedFactory,
onWorkspaceJoinedFactory
onWorkspaceRoleUpdatedFactory
} from '@/modules/workspaces/events/eventListener'
import { expect } from 'chai'
import { mapWorkspaceRoleToInitialProjectRole } from '@/modules/workspaces/domain/logic'
@@ -61,16 +61,22 @@ describe('Event handlers', () => {
expect(projectRoles.length).to.equal(2)
})
})
describe('onWorkspaceJoinedFactory creates a function, that', () => {
describe('onWorkspaceRoleUpdatedFactory creates a function, that', () => {
it('assigns no project roles if the role mapping returns null', async () => {
await onWorkspaceJoinedFactory({
let isDeleteCalled = false
await onWorkspaceRoleUpdatedFactory({
getDefaultWorkspaceProjectRoleMapping: async () => ({
[Roles.Workspace.Admin]: Roles.Stream.Owner,
[Roles.Workspace.Member]: Roles.Stream.Contributor,
[Roles.Workspace.Guest]: null
}),
async *queryAllWorkspaceProjects() {
expect.fail()
yield [{ id: 'test' } as StreamRecord]
},
deleteProjectRole: async () => {
isDeleteCalled = true
return undefined
},
upsertProjectRole: async () => {
expect.fail()
@@ -80,6 +86,8 @@ describe('Event handlers', () => {
userId: cryptoRandomString({ length: 10 }),
workspaceId: cryptoRandomString({ length: 10 })
})
expect(isDeleteCalled).to.be.true
})
it('assigns the mapped projects roles to all queried project', async () => {
const projectIds = [
@@ -92,7 +100,7 @@ describe('Event handlers', () => {
const projectRole = Roles.Stream.Reviewer
const storedRoles: { userId: string; role: StreamRoles; projectId: string }[] = []
await onWorkspaceJoinedFactory({
await onWorkspaceRoleUpdatedFactory({
getDefaultWorkspaceProjectRoleMapping: async () => ({
[Roles.Workspace.Admin]: Roles.Stream.Owner,
[Roles.Workspace.Member]: projectRole,
@@ -103,6 +111,9 @@ describe('Event handlers', () => {
yield projIds.map((projId) => ({ id: projId } as unknown as StreamRecord))
}
},
deleteProjectRole: async () => {
expect.fail()
},
upsertProjectRole: async (args) => {
storedRoles.push(args)
return {} as StreamRecord
@@ -14,7 +14,10 @@ import {
import { Roles } from '@speckle/shared'
import { expect } from 'chai'
import cryptoRandomString from 'crypto-random-string'
import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events'
import {
WorkspaceEvents,
WorkspaceEventsPayloads
} from '@/modules/workspacesCore/domain/events'
import { StreamAclRecord, StreamRecord } from '@/modules/core/helpers/types'
import { expectToThrow } from '@/test/assertionHelper'
import { createRandomPassword } from '@/modules/core/helpers/testHelpers'
@@ -386,17 +389,21 @@ const buildDeleteWorkspaceRoleAndTestContext = (
context.eventData.eventName = eventName
context.eventData.payload = payload
switch (eventName) {
case 'workspace.role-deleted': {
const { userId } =
payload as WorkspaceEventsPayloads['workspace.role-deleted']
for (const project of context.workspaceProjects) {
context.workspaceProjectRoles = context.workspaceProjectRoles.filter(
(role) => role.resourceId !== project.id && role.userId !== userId
)
}
break
}
}
return []
},
async *queryAllWorkspaceProjects() {
yield context.workspaceProjects
},
deleteProjectRole: async ({ projectId, userId }) => {
context.workspaceProjectRoles = context.workspaceProjectRoles.filter(
(role) => role.resourceId !== projectId && role.userId !== userId
)
return {} as StreamRecord
},
...dependencyOverrides
}
@@ -430,32 +437,47 @@ const buildUpdateWorkspaceRoleAndTestContext = (
context.eventData.eventName = eventName
context.eventData.payload = payload
return []
},
async *queryAllWorkspaceProjects() {
yield context.workspaceProjects
},
getDefaultWorkspaceProjectRoleMapping: mapWorkspaceRoleToInitialProjectRole,
upsertProjectRole: async (role) => {
const streamAcl: StreamAclRecord = {
userId: role.userId,
role: role.role,
resourceId: role.projectId
switch (eventName) {
case 'workspace.role-deleted': {
const { userId } =
payload as WorkspaceEventsPayloads['workspace.role-deleted']
for (const project of context.workspaceProjects) {
context.workspaceProjectRoles = context.workspaceProjectRoles.filter(
(role) => role.resourceId !== project.id && role.userId !== userId
)
}
break
}
case 'workspace.role-updated': {
const workspaceRole =
payload as WorkspaceEventsPayloads['workspace.role-updated']
const mapping = await mapWorkspaceRoleToInitialProjectRole({
workspaceId: workspaceRole.workspaceId
})
for (const project of context.workspaceProjects) {
const projectRole = mapping[workspaceRole.role]
if (!projectRole) {
continue
}
const streamAcl: StreamAclRecord = {
userId: workspaceRole.userId,
role: projectRole,
resourceId: project.id
}
context.workspaceProjectRoles = context.workspaceProjectRoles.filter(
(acl) => acl.userId !== workspaceRole.userId
)
context.workspaceProjectRoles.push(streamAcl)
}
break
}
}
context.workspaceProjectRoles = context.workspaceProjectRoles.filter(
(acl) => acl.userId !== role.userId
)
context.workspaceProjectRoles.push(streamAcl)
return {} as StreamRecord
},
deleteProjectRole: async ({ userId }) => {
context.workspaceProjectRoles = context.workspaceProjectRoles.filter(
(acl) => acl.userId !== userId
)
return {} as StreamRecord
return []
},
...dependencyOverrides
}
@@ -589,9 +611,15 @@ describe('Workspace role services', () => {
await updateWorkspaceRole(role)
const payload = {
...(context.eventData
.payload as WorkspaceEventsPayloads['workspace.role-updated'])
}
delete payload.flags
expect(context.eventData.isCalled).to.be.true
expect(context.eventData.eventName).to.equal(WorkspaceEvents.RoleUpdated)
expect(context.eventData.payload).to.deep.equal(role)
expect(payload).to.deep.equal(role)
})
it('throws if attempting to remove the last admin in a workspace', async () => {
const userId = cryptoRandomString({ length: 10 })