From cf833a77196852dadf73e50fee93caefac3561a3 Mon Sep 17 00:00:00 2001 From: Kristaps Fabians Geikins Date: Tue, 29 Apr 2025 10:49:37 +0300 Subject: [PATCH] fix(server): project role updates after workspace role/seat changes (#4599) * fix(workspaces): workspace role sync * role changes fixed + validated * seat changes validated * fix tests --------- Co-authored-by: Charles Driesler --- packages/server/codegen.yml | 1 + .../modules/core/repositories/streams.ts | 4 +- .../modules/shared/helpers/envHelper.ts | 2 +- .../modules/workspaces/domain/operations.ts | 4 + .../workspaces/events/eventListener.ts | 255 +++- .../workspaces/graph/resolvers/workspaces.ts | 104 +- .../workspaces/repositories/workspaces.ts | 8 +- .../modules/workspaces/services/management.ts | 14 +- .../modules/workspaces/services/projects.ts | 2 +- .../workspaces/tests/helpers/creation.ts | 3 +- .../workspaces/tests/helpers/rolesGraphql.ts | 112 ++ .../tests/integration/roles.graph.spec.ts | 1257 +++++++++++------ .../tests/unit/events/eventListener.spec.ts | 136 +- .../tests/unit/services/management.spec.ts | 38 +- .../modules/workspacesCore/domain/events.ts | 3 +- .../server/test/graphql/generated/graphql.ts | 35 + utils/helm/speckle-server/values.schema.json | 2 +- utils/helm/speckle-server/values.yaml | 2 +- 18 files changed, 1328 insertions(+), 654 deletions(-) create mode 100644 packages/server/modules/workspaces/tests/helpers/rolesGraphql.ts diff --git a/packages/server/codegen.yml b/packages/server/codegen.yml index fcc3c7d89..11ac09d9d 100644 --- a/packages/server/codegen.yml +++ b/packages/server/codegen.yml @@ -118,6 +118,7 @@ generates: documents: - 'test/graphql/*.{js,ts}' - 'modules/**/tests/helpers/graphql.ts' + - 'modules/**/tests/helpers/*Graphql.ts' config: enumsAsConst: true scalars: diff --git a/packages/server/modules/core/repositories/streams.ts b/packages/server/modules/core/repositories/streams.ts index 7bbe9e6e9..02cadc776 100644 --- a/packages/server/modules/core/repositories/streams.ts +++ b/packages/server/modules/core/repositories/streams.ts @@ -1144,7 +1144,7 @@ export const grantStreamPermissionsFactory = .count() if (parseInt(countObj.count as string) === 1) throw new StreamAccessUpdateError( - 'Could not revoke permissions for last admin', + 'A project needs at least one project owner', { info: { streamId, userId } } @@ -1222,7 +1222,7 @@ export const revokeStreamPermissionsFactory = .count() if (parseInt(countObj.count as string) === 1) throw new StreamAccessUpdateError( - 'Could not revoke permissions for last admin', + 'A project needs at least one project owner', { info: { streamId, userId } } diff --git a/packages/server/modules/shared/helpers/envHelper.ts b/packages/server/modules/shared/helpers/envHelper.ts index 35ec3b211..a9c5942c9 100644 --- a/packages/server/modules/shared/helpers/envHelper.ts +++ b/packages/server/modules/shared/helpers/envHelper.ts @@ -374,7 +374,7 @@ export function isEmailEnabled() { } export function postgresMaxConnections() { - return getIntFromEnv('POSTGRES_MAX_CONNECTIONS_SERVER', '4') + return getIntFromEnv('POSTGRES_MAX_CONNECTIONS_SERVER', '8') } export function postgresConnectionAcquireTimeoutMillis() { diff --git a/packages/server/modules/workspaces/domain/operations.ts b/packages/server/modules/workspaces/domain/operations.ts index e20029883..bb3f51f1a 100644 --- a/packages/server/modules/workspaces/domain/operations.ts +++ b/packages/server/modules/workspaces/domain/operations.ts @@ -124,6 +124,10 @@ export type GetWorkspaceCollaboratorsArgs = { */ search?: string seatType?: WorkspaceSeatType + /** + * Optionally filter by user id + */ + excludeUserIds?: string[] } } diff --git a/packages/server/modules/workspaces/events/eventListener.ts b/packages/server/modules/workspaces/events/eventListener.ts index d04b85285..d94520d32 100644 --- a/packages/server/modules/workspaces/events/eventListener.ts +++ b/packages/server/modules/workspaces/events/eventListener.ts @@ -1,5 +1,4 @@ import { - deleteProjectRoleFactory, getStreamFactory, getStreamsCollaboratorCountsFactory, grantStreamPermissionsFactory, @@ -11,6 +10,7 @@ import { CountWorkspaceRoleWithOptionalProjectRole, GetDefaultRegion, GetWorkspace, + GetWorkspaceCollaborators, GetWorkspaceRoleForUser, GetWorkspaceRoleToDefaultProjectRoleMapping, GetWorkspaceSeatTypeToProjectRoleMapping, @@ -29,15 +29,18 @@ import { logger, moduleLogger } from '@/observability/logging' import { updateWorkspaceRoleFactory } from '@/modules/workspaces/services/management' import { EventPayload, getEventBus } from '@/modules/shared/services/eventBus' import { WorkspaceInviteResourceType } from '@/modules/workspacesCore/domain/constants' -import { Roles, throwUncoveredError, WorkspaceRoles } from '@speckle/shared' import { - DeleteProjectRole, - UpsertProjectRole -} from '@/modules/core/domain/projects/operations' + Roles, + StreamRoles, + throwUncoveredError, + WorkspaceRoles +} from '@speckle/shared' +import { UpsertProjectRole } from '@/modules/core/domain/projects/operations' import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events' import { Knex } from 'knex' import { countWorkspaceRoleWithOptionalProjectRoleFactory, + getWorkspaceCollaboratorsFactory, getWorkspaceFactory, getWorkspaceRoleForUserFactory, getWorkspaceRolesFactory, @@ -86,7 +89,7 @@ import { GetWorkspaceWithPlan } from '@/modules/gatekeeper/domain/billing' import { getWorkspacePlanProductId } from '@/modules/gatekeeper/stripe' -import { Workspace } from '@/modules/workspacesCore/domain/types' +import { Workspace, WorkspaceSeatType } from '@/modules/workspacesCore/domain/types' import { FindEmailsByUserId } from '@/modules/core/domain/userEmails/operations' import { getDefaultRegionFactory } from '@/modules/workspaces/repositories/regions' import { @@ -103,7 +106,10 @@ import { getWorkspaceRolesAndSeatsFactory, getWorkspaceUserSeatFactory } from '@/modules/gatekeeper/repositories/workspaceSeat' -import { DeleteWorkspaceSeat } from '@/modules/gatekeeper/domain/operations' +import { + DeleteWorkspaceSeat, + GetWorkspaceUserSeat +} from '@/modules/gatekeeper/domain/operations' import { isStreamCollaboratorFactory, setStreamCollaboratorFactory, @@ -202,9 +208,9 @@ export const onInviteFinalizedFactory = role: workspaceRole, userId: targetUserId, workspaceId: project.workspaceId, - skipProjectRoleUpdatesFor: [project.id], preventRoleDowngrade: true, - updatedByUserId: invite.inviterId + updatedByUserId: invite.inviterId, + skipProjectRoleUpdatesFor: [project.id] }) // Automatically promote user to project owner if workspace admin @@ -256,29 +262,73 @@ export const onWorkspaceAuthorizedFactory = } export const onWorkspaceRoleDeletedFactory = - ({ - queryAllWorkspaceProjects, - deleteProjectRole, - deleteWorkspaceSeat - }: { + (deps: { queryAllWorkspaceProjects: QueryAllWorkspaceProjects - deleteProjectRole: DeleteProjectRole deleteWorkspaceSeat: DeleteWorkspaceSeat + getStreamsCollaboratorCounts: GetStreamsCollaboratorCounts + getWorkspaceCollaborators: GetWorkspaceCollaborators + setStreamCollaborator: SetStreamCollaborator }) => - async ({ userId, workspaceId }: { userId: string; workspaceId: string }) => { + async ({ + acl: { userId, workspaceId }, + updatedByUserId + }: { + acl: { userId: string; workspaceId: string } + updatedByUserId: string + }) => { + // Resolve a fallback admin + const [admin] = await deps.getWorkspaceCollaborators({ + workspaceId, + limit: 1, + filter: { + roles: [Roles.Workspace.Admin], + excludeUserIds: [userId] + } + }) + // Delete roles for all workspace projects - for await (const projectsPage of queryAllWorkspaceProjects({ - workspaceId + for await (const projectsPage of deps.queryAllWorkspaceProjects({ + workspaceId, + userId })) { + const projectsOldOwnerCounts = await deps.getStreamsCollaboratorCounts({ + streamIds: projectsPage.map((p) => p.id), + type: Roles.Stream.Owner + }) await Promise.all( - projectsPage.map(({ id: projectId }) => - deleteProjectRole({ projectId, userId }) - ) + projectsPage.map(async ({ id: projectId, role: originalProjectRole }) => { + // If downgraded from owner & last owner, transfer ownership to a workspace admin + const isNoLongerOwner = originalProjectRole === Roles.Stream.Owner + const wasLastOwner = + projectsOldOwnerCounts[projectId]?.[Roles.Stream.Owner] === 1 + if (isNoLongerOwner && wasLastOwner) { + await deps.setStreamCollaborator( + { + streamId: projectId, + userId: admin.id, + role: Roles.Stream.Owner, + setByUserId: updatedByUserId + }, + { trackProjectUpdate: false, skipAuthorization: true } + ) + } + + // Do actual role change for changed user + await deps.setStreamCollaborator( + { + streamId: projectId, + userId, + role: null, + setByUserId: updatedByUserId + }, + { trackProjectUpdate: false, skipAuthorization: true } + ) + }) ) } // Delete seat - await deleteWorkspaceSeat({ userId, workspaceId }) + await deps.deleteWorkspaceSeat({ userId, workspaceId }) } export const onWorkspaceSeatUpdatedFactory = @@ -288,6 +338,8 @@ export const onWorkspaceSeatUpdatedFactory = setStreamCollaborator: SetStreamCollaborator getWorkspaceWithPlan: GetWorkspaceWithPlan getWorkspaceRoleForUser: GetWorkspaceRoleForUser + getStreamsCollaboratorCounts: GetStreamsCollaboratorCounts + getWorkspaceCollaborators: GetWorkspaceCollaborators }) => async (params: EventPayload) => { const { seat, updatedByUserId } = params.payload @@ -310,11 +362,25 @@ export const onWorkspaceSeatUpdatedFactory = workspaceId }) + // Resolve a fallback admin + const [admin] = await deps.getWorkspaceCollaborators({ + workspaceId, + limit: 1, + filter: { + roles: [Roles.Workspace.Admin], + excludeUserIds: [userId] + } + }) + // Ensure project roles are valid on seat type switch for await (const projectsPage of deps.queryAllWorkspaceProjects({ workspaceId, userId })) { + const projectsOldOwnerCounts = await deps.getStreamsCollaboratorCounts({ + streamIds: projectsPage.map((p) => p.id), + type: Roles.Stream.Owner + }) await Promise.all( projectsPage.map(async ({ id: projectId, role: originalProjectRole }) => { const disallowedProjectRole = @@ -322,12 +388,32 @@ export const onWorkspaceSeatUpdatedFactory = !allowedProjectRoles[seatType].includes(originalProjectRole) if (!disallowedProjectRole) return - const newRole = defaultProjectRoles[seatType] + const nextUserRole = defaultProjectRoles[seatType] + + // If downgraded from owner & last owner, transfer ownership to a workspace admin + const isNoLongerOwner = + originalProjectRole === Roles.Stream.Owner && + nextUserRole !== Roles.Stream.Owner + const wasLastOwner = + projectsOldOwnerCounts[projectId]?.[Roles.Stream.Owner] === 1 + if (isNoLongerOwner && wasLastOwner) { + await deps.setStreamCollaborator( + { + streamId: projectId, + userId: admin.id, + role: Roles.Stream.Owner, + setByUserId: updatedByUserId + }, + { trackProjectUpdate: false, skipAuthorization: true } + ) + } + + // Do actual role change for changed user await deps.setStreamCollaborator( { streamId: projectId, userId, - role: newRole, + role: nextUserRole, setByUserId: updatedByUserId }, { trackProjectUpdate: false, skipAuthorization: true } @@ -342,13 +428,15 @@ export const onWorkspaceRoleUpdatedFactory = getWorkspaceRoleToDefaultProjectRoleMapping: GetWorkspaceRoleToDefaultProjectRoleMapping queryAllWorkspaceProjects: QueryAllWorkspaceProjects setStreamCollaborator: SetStreamCollaborator + getWorkspaceUserSeat: GetWorkspaceUserSeat getStreamsCollaboratorCounts: GetStreamsCollaboratorCounts + getWorkspaceCollaborators: GetWorkspaceCollaborators getWorkspaceWithPlan: GetWorkspaceWithPlan }) => async ({ acl, - flags, - updatedByUserId + updatedByUserId, + flags }: { acl: { userId: string; role: WorkspaceRoles; workspaceId: string } flags?: { @@ -357,23 +445,31 @@ export const onWorkspaceRoleUpdatedFactory = updatedByUserId: string }) => { const { userId, role, workspaceId } = acl + const workspace = await deps.getWorkspaceWithPlan({ workspaceId }) if (!workspace) return - // New plans don't do automatic project role assignment - const isNewPlan = workspace.plan && isNewPlanType(workspace.plan.name) - if (isNewPlan) { - return - } - + // Until we kill old plan code, we need to do full project role assignment for them + const isOldPlan = !workspace.plan || !isNewPlanType(workspace.plan.name) const { default: defaultProjectRoles } = await deps.getWorkspaceRoleToDefaultProjectRoleMapping({ workspaceId }) - const nextUserRole = defaultProjectRoles[role] + const seatType = await deps.getWorkspaceUserSeat({ workspaceId, userId }) + if (!seatType) return - // Keep user's project roles in sync with their workspace role + // Resolve a fallback admin + const [admin] = await deps.getWorkspaceCollaborators({ + workspaceId, + limit: 1, + filter: { + roles: [Roles.Workspace.Admin], + excludeUserIds: [userId] + } + }) + + // Enforce project roles based on workspace role and seat type, if project role exists for await (const projectsPage of deps.queryAllWorkspaceProjects({ workspaceId, userId @@ -382,26 +478,64 @@ export const onWorkspaceRoleUpdatedFactory = streamIds: projectsPage.map((p) => p.id), type: Roles.Stream.Owner }) - await Promise.all( projectsPage.map(async ({ id: projectId, role: originalProjectRole }) => { - if (flags?.skipProjectRoleUpdatesFor.includes(projectId)) { + if (isOldPlan && flags?.skipProjectRoleUpdatesFor.includes(projectId)) { // Skip assignment (used during invite flow) // TODO: Can we refactor this special case away? return } - // If downgraded from owner & last owner, transfer ownership to admin causing the role update (updatedByUserId) + if (!originalProjectRole && !isOldPlan) { + return + } + + /** + * We cant really throw here, because by this point the workspace role has already + * been written to DB. So we must ensure the updates we make here are valid + */ + + let nextUserRole: StreamRoles | null + if (isOldPlan) { + nextUserRole = defaultProjectRoles[role] + } else { + switch (role) { + case Roles.Workspace.Admin: { + // Set workspace owner as project owner + nextUserRole = Roles.Stream.Owner + break + } + case Roles.Workspace.Guest: { + // If workspace guest is project owner + if (originalProjectRole !== Roles.Stream.Owner) { + return + } + + // If workspace guest has an editor seat + if (seatType.type !== WorkspaceSeatType.Editor) { + return + } + + // Demote to contributor + nextUserRole = Roles.Stream.Contributor + break + } + default: + return + } + } + + // If downgraded from owner & last owner, transfer ownership to a workspace admin const isNoLongerOwner = originalProjectRole === Roles.Stream.Owner && - (!nextUserRole || nextUserRole !== Roles.Stream.Owner) + nextUserRole !== Roles.Stream.Owner const wasLastOwner = projectsOldOwnerCounts[projectId]?.[Roles.Stream.Owner] === 1 if (isNoLongerOwner && wasLastOwner) { await deps.setStreamCollaborator( { streamId: projectId, - userId: updatedByUserId, + userId: admin.id, role: Roles.Stream.Owner, setByUserId: updatedByUserId }, @@ -409,7 +543,7 @@ export const onWorkspaceRoleUpdatedFactory = ) } - // Finally change target role + // Do actual role change for changed user await deps.setStreamCollaborator( { streamId: projectId, @@ -735,11 +869,28 @@ export const initializeEventListenersFactory = queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ getStreams }), - deleteProjectRole: deleteProjectRoleFactory({ db: trx }), - deleteWorkspaceSeat: deleteWorkspaceSeatFactory({ db: trx }) + deleteWorkspaceSeat: deleteWorkspaceSeatFactory({ db: trx }), + getStreamsCollaboratorCounts: getStreamsCollaboratorCountsFactory({ db }), + getWorkspaceCollaborators: getWorkspaceCollaboratorsFactory({ db }), + setStreamCollaborator: setStreamCollaboratorFactory({ + getUser: getUserFactory({ db }), + validateStreamAccess: validateStreamAccessFactory({ + authorizeResolver + }), + emitEvent: eventBus.emit, + grantStreamPermissions: grantStreamPermissionsFactory({ + db: trx + }), + isStreamCollaborator: isStreamCollaboratorFactory({ + getStream: getStreamFactory({ db }) + }), + revokeStreamPermissions: revokeStreamPermissionsFactory({ + db: trx + }) + }) }) - return await onWorkspaceRoleDeleted(payload.acl) + return await onWorkspaceRoleDeleted(payload) }, { db } ) @@ -748,11 +899,6 @@ export const initializeEventListenersFactory = await withTransaction( async ({ db: trx }) => { const onWorkspaceRoleUpdated = onWorkspaceRoleUpdatedFactory({ - getWorkspaceWithPlan: getWorkspaceWithPlanFactory({ db }), - getWorkspaceRoleToDefaultProjectRoleMapping: - getWorkspaceRoleToDefaultProjectRoleMappingFactory({ - getWorkspaceWithPlan: getWorkspaceWithPlanFactory({ db }) - }), queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ getStreams }), @@ -772,7 +918,14 @@ export const initializeEventListenersFactory = db: trx }) }), - getStreamsCollaboratorCounts: getStreamsCollaboratorCountsFactory({ db }) + getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db }), + getStreamsCollaboratorCounts: getStreamsCollaboratorCountsFactory({ db }), + getWorkspaceCollaborators: getWorkspaceCollaboratorsFactory({ db }), + getWorkspaceWithPlan: getWorkspaceWithPlanFactory({ db }), + getWorkspaceRoleToDefaultProjectRoleMapping: + getWorkspaceRoleToDefaultProjectRoleMappingFactory({ + getWorkspaceWithPlan: getWorkspaceWithPlanFactory({ db }) + }) }) return await onWorkspaceRoleUpdated(payload) }, @@ -803,7 +956,9 @@ export const initializeEventListenersFactory = getWorkspaceSeatTypeToProjectRoleMapping: getWorkspaceSeatTypeToProjectRoleMappingFactory({ getWorkspaceWithPlan: getWorkspaceWithPlanFactory({ db }) - }) + }), + getStreamsCollaboratorCounts: getStreamsCollaboratorCountsFactory({ db }), + getWorkspaceCollaborators: getWorkspaceCollaboratorsFactory({ db }) }) return await onWorkspaceSeatUpdated(payload) diff --git a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts index 687085615..0aea2092b 100644 --- a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts +++ b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts @@ -148,7 +148,6 @@ import { updateStreamRoleAndNotifyFactory } from '@/modules/core/services/stream import { getUserFactory, getUsersFactory } from '@/modules/core/repositories/users' import { getServerInfoFactory } from '@/modules/core/repositories/server' import { asOperation, commandFactory } from '@/modules/shared/command' -import { withTransaction } from '@/modules/shared/helpers/dbHelper' import { getRateLimitResult, isRateLimitBreached @@ -728,37 +727,35 @@ export = FF_WORKSPACES_MODULE_ENABLED }) if (!role) { - // this is currently not working with the command factory - // TODO: include the onWorkspaceRoleDeletedFactory listener service - await withOperationLogging( - async () => - await withTransaction( - async ({ db: trx }) => { - const deleteWorkspaceRole = deleteWorkspaceRoleFactory({ - deleteWorkspaceRole: repoDeleteWorkspaceRoleFactory({ db: trx }), - getWorkspaceRoles: getWorkspaceRolesFactory({ db: trx }), - emitWorkspaceEvent: getEventBus().emit - }) + await asOperation( + async ({ db, emit }) => { + const deleteWorkspaceRole = deleteWorkspaceRoleFactory({ + deleteWorkspaceRole: repoDeleteWorkspaceRoleFactory({ db }), + getWorkspaceRoles: getWorkspaceRolesFactory({ db }), + emitWorkspaceEvent: emit + }) - return await deleteWorkspaceRole({ workspaceId, userId }) - }, - { db } - ), + return await deleteWorkspaceRole({ + workspaceId, + userId, + deletedByUserId: context.userId! + }) + }, { logger, - operationName: 'deleteWorkspaceRole', - operationDescription: 'Delete workspace role' + name: 'deleteWorkspaceRole', + description: 'Delete workspace role', + transaction: true } ) } else { if (!isWorkspaceRole(role)) { throw new WorkspaceInvalidRoleError() } - const updateWorkspaceRole = commandFactory({ - db, - eventBus, - operationFactory: ({ trx, emit }) => - updateWorkspaceRoleFactory({ + + await asOperation( + async ({ db: trx, emit }) => { + const updateWorkspaceRole = updateWorkspaceRoleFactory({ upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db: trx }), getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db: trx }), findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ @@ -772,23 +769,25 @@ export = FF_WORKSPACES_MODULE_ENABLED eventEmit: emit }) }) - }) - await withOperationLogging( - async () => - await updateWorkspaceRole({ + + return await updateWorkspaceRole({ userId, workspaceId, role, updatedByUserId: context.userId! - }), + }) + }, { logger, - operationName: 'updateWorkspaceRole', - operationDescription: 'Update workspace role' + name: 'updateWorkspaceRole', + description: 'Update workspace role', + transaction: true } ) } + context.clearCache() + return await getWorkspaceFactory({ db })({ workspaceId: args.input.workspaceId, userId: context.userId @@ -943,31 +942,30 @@ export = FF_WORKSPACES_MODULE_ENABLED const logger = context.log.child({ workspaceId }) - // this is currently not working with the command factory - // TODO: include the onWorkspaceRoleDeletedFactory listener service - await withOperationLogging( - async () => - await withTransaction( - async ({ db: trx }) => { - const deleteWorkspaceRole = deleteWorkspaceRoleFactory({ - deleteWorkspaceRole: repoDeleteWorkspaceRoleFactory({ db: trx }), - getWorkspaceRoles: getWorkspaceRolesFactory({ db: trx }), - emitWorkspaceEvent: getEventBus().emit - }) + await asOperation( + async ({ db, emit }) => { + const deleteWorkspaceRole = deleteWorkspaceRoleFactory({ + deleteWorkspaceRole: repoDeleteWorkspaceRoleFactory({ db }), + getWorkspaceRoles: getWorkspaceRolesFactory({ db }), + emitWorkspaceEvent: emit + }) - return await deleteWorkspaceRole({ - workspaceId, - userId: context.userId! - }) - }, - { db } - ), + return await deleteWorkspaceRole({ + workspaceId, + userId: context.userId!, + deletedByUserId: context.userId! + }) + }, { logger, - operationName: 'leaveWorkspace', - operationDescription: 'Leave workspace' + name: 'leaveWorkspace', + description: 'Leave workspace', + transaction: true } ) + + context.clearCache() + return true }, updateCreationState: async (_parent, args, context) => { @@ -1394,7 +1392,7 @@ export = FF_WORKSPACES_MODULE_ENABLED projectId, streamId: projectId //legacy }) - return await withOperationLogging( + const ret = await withOperationLogging( async () => await updateStreamRoleAndNotify( args.input, @@ -1407,6 +1405,10 @@ export = FF_WORKSPACES_MODULE_ENABLED operationDescription: 'Update workspace project role' } ) + + context.clearCache() + + return ret }, moveToWorkspace: async (_parent, args, context) => { const { projectId, workspaceId } = args diff --git a/packages/server/modules/workspaces/repositories/workspaces.ts b/packages/server/modules/workspaces/repositories/workspaces.ts index a18b8a596..7e965a990 100644 --- a/packages/server/modules/workspaces/repositories/workspaces.ts +++ b/packages/server/modules/workspaces/repositories/workspaces.ts @@ -364,7 +364,7 @@ export const getWorkspaceCollaboratorsFactory = .where(DbWorkspaceAcl.col.workspaceId, workspaceId) .orderBy('workspaceRoleCreatedAt', 'desc') - const { search, roles, seatType } = filter || {} + const { search, roles, seatType, excludeUserIds } = filter || {} if (seatType) { query @@ -387,6 +387,12 @@ export const getWorkspaceCollaboratorsFactory = }) } + if (excludeUserIds?.length) { + query.andWhere((w) => { + w.whereNotIn(Users.col.id, excludeUserIds) + }) + } + if (cursor) { query.andWhere(DbWorkspaceAcl.col.createdAt, '<', cursor) } diff --git a/packages/server/modules/workspaces/services/management.ts b/packages/server/modules/workspaces/services/management.ts index d7d1700d6..baa4aeffe 100644 --- a/packages/server/modules/workspaces/services/management.ts +++ b/packages/server/modules/workspaces/services/management.ts @@ -349,6 +349,7 @@ export const deleteWorkspaceFactory = type WorkspaceRoleDeleteArgs = { userId: string workspaceId: string + deletedByUserId: string } export const deleteWorkspaceRoleFactory = @@ -363,7 +364,8 @@ export const deleteWorkspaceRoleFactory = }) => async ({ workspaceId, - userId + userId, + deletedByUserId }: WorkspaceRoleDeleteArgs): Promise => { // Protect against removing last admin const workspaceRoles = await getWorkspaceRoles({ workspaceId }) @@ -380,7 +382,7 @@ export const deleteWorkspaceRoleFactory = // Emit deleted role await emitWorkspaceEvent({ eventName: WorkspaceEvents.RoleDeleted, - payload: { acl: deletedRole } + payload: { acl: deletedRole, updatedByUserId: deletedByUserId } }) return deletedRole @@ -420,9 +422,9 @@ export const updateWorkspaceRoleFactory = workspaceId, userId, role: nextWorkspaceRole, - skipProjectRoleUpdatesFor, preventRoleDowngrade, - updatedByUserId + updatedByUserId, + skipProjectRoleUpdatesFor }): Promise => { const workspaceRoles = await getWorkspaceRoles({ workspaceId }) @@ -493,10 +495,10 @@ export const updateWorkspaceRoleFactory = workspaceId, role: nextWorkspaceRole }, + updatedByUserId, flags: { skipProjectRoleUpdatesFor: skipProjectRoleUpdatesFor ?? [] - }, - updatedByUserId + } } }) } diff --git a/packages/server/modules/workspaces/services/projects.ts b/packages/server/modules/workspaces/services/projects.ts index 25573eac8..aea08cc37 100644 --- a/packages/server/modules/workspaces/services/projects.ts +++ b/packages/server/modules/workspaces/services/projects.ts @@ -356,7 +356,7 @@ export const validateWorkspaceMemberProjectRoleFactory = // 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 seat type '${seatType}' and workspace role '${workspaceRole}' does not allow project role '${projectRole}'.` : `User's workspace role '${workspaceRole}' does not allow project role '${projectRole}'.` ) } diff --git a/packages/server/modules/workspaces/tests/helpers/creation.ts b/packages/server/modules/workspaces/tests/helpers/creation.ts index 2e43ad91d..347bc6261 100644 --- a/packages/server/modules/workspaces/tests/helpers/creation.ts +++ b/packages/server/modules/workspaces/tests/helpers/creation.ts @@ -339,7 +339,8 @@ export const unassignFromWorkspace = async ( await deleteWorkspaceRole({ userId: user.id, - workspaceId: workspace.id + workspaceId: workspace.id, + deletedByUserId: workspace.ownerId }) } diff --git a/packages/server/modules/workspaces/tests/helpers/rolesGraphql.ts b/packages/server/modules/workspaces/tests/helpers/rolesGraphql.ts new file mode 100644 index 000000000..e06546498 --- /dev/null +++ b/packages/server/modules/workspaces/tests/helpers/rolesGraphql.ts @@ -0,0 +1,112 @@ +import { basicWorkspaceFragment } from '@/modules/workspaces/tests/helpers/graphql' +import { ProjectImplicitRoleCheckFragment } from '@/test/graphql/generated/graphql' +import { MaybeNullOrUndefined, Roles } from '@speckle/shared' +import { gql } from 'graphql-tag' + +export const fullPermissionCheckResultFragment = gql(` + fragment FullPermissionCheckResult on PermissionCheckResult { + authorized + code + message + payload + } +`) + +export const projectImplicitRoleCheckFragment = gql` + fragment ProjectImplicitRoleCheck on Project { + id + role + permissions { + # general access check + canRead { + ...FullPermissionCheckResult + } + # implicit reviewer check + canReadSettings { + ...FullPermissionCheckResult + } + # implicit owner check + canReadWebhooks { + ...FullPermissionCheckResult + } + # implicit contributor check + canCreateModel { + ...FullPermissionCheckResult + } + } + } + + ${fullPermissionCheckResultFragment} +` + +export const getUserWorkspaceAccessQuery = gql` + query GetUserWorkspaceAccess($id: String!) { + workspace(id: $id) { + id + role + seatType + } + } +` + +export const getUserWorkspaceProjectsWithAccessChecksQuery = gql` + query GetUserWorkspaceProjectsWithAccessChecks( + $id: String! + $limit: Int + $cursor: String + $filter: WorkspaceProjectsFilter + ) { + workspace(id: $id) { + ...BasicWorkspace + role + seatType + projects(limit: $limit, cursor: $cursor, filter: $filter) { + items { + ...ProjectImplicitRoleCheck + } + cursor + totalCount + } + } + } + + ${basicWorkspaceFragment} + ${projectImplicitRoleCheckFragment} +` + +export const getUserProjectsWithAccessChecksQuery = gql` + query GetUserProjectsWithAccessChecks( + $limit: Int + $cursor: String + $filter: UserProjectsFilter + ) { + activeUser { + id + projects(limit: $limit, cursor: $cursor, filter: $filter) { + items { + ...ProjectImplicitRoleCheck + } + cursor + totalCount + } + } + } + ${projectImplicitRoleCheckFragment} +` + +export const projectImplicitRoleCheck = ( + project: MaybeNullOrUndefined +) => { + return { + hasAccess: !!project?.permissions?.canRead.authorized, + isReviewer: !!project?.permissions?.canReadSettings.authorized, + isContributor: !!project?.permissions?.canCreateModel.authorized, + isOwner: !!project?.permissions?.canReadWebhooks.authorized, + isExplicitOwner: project?.role === Roles.Stream.Owner, + isExplicitContributor: project?.role === Roles.Stream.Contributor, + isExplicitReviewer: project?.role === Roles.Stream.Reviewer, + hasExplicitRole: !!project?.role + } +} + +export type ProjectImplicitRoleCheck = ReturnType diff --git a/packages/server/modules/workspaces/tests/integration/roles.graph.spec.ts b/packages/server/modules/workspaces/tests/integration/roles.graph.spec.ts index 0d7af2595..e929a6183 100644 --- a/packages/server/modules/workspaces/tests/integration/roles.graph.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/roles.graph.spec.ts @@ -1,11 +1,21 @@ -import { db } from '@/db/knex' +import { Streams } from '@/modules/core/dbSchema' import { AllScopes } from '@/modules/core/helpers/mainConstants' -import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams' import { assignToWorkspace, BasicTestWorkspace, - createTestWorkspace + createTestWorkspace, + unassignFromWorkspace } from '@/modules/workspaces/tests/helpers/creation' +import { + ProjectImplicitRoleCheck, + projectImplicitRoleCheck +} from '@/modules/workspaces/tests/helpers/rolesGraphql' +import { WorkspaceSeatType } from '@/modules/workspacesCore/domain/types' +import { + WorkspaceAcl, + Workspaces, + WorkspaceSeats +} from '@/modules/workspacesCore/helpers/db' import { BasicTestUser, createAuthTokenForUser, @@ -13,10 +23,12 @@ import { } from '@/test/authHelper' import { ActiveUserLeaveWorkspaceDocument, + GetUserProjectsWithAccessChecksDocument, + GetUserWorkspaceAccessDocument, + GetUserWorkspaceProjectsWithAccessChecksDocument, GetWorkspaceDocument, - GetWorkspaceProjectsDocument, - GetWorkspaceTeamDocument, - UpdateWorkspaceRoleDocument + UpdateWorkspaceRoleDocument, + UpdateWorkspaceSeatTypeDocument } from '@/test/graphql/generated/graphql' import { createTestContext, @@ -24,15 +36,16 @@ import { TestApolloServer } from '@/test/graphqlHelper' import { beforeEachContext, truncateTables } from '@/test/hooks' -import { BasicTestStream, createTestStream } from '@/test/speckle-helpers/streamHelper' +import { + addToStream, + BasicTestStream, + createTestStream +} from '@/test/speckle-helpers/streamHelper' import { Roles } from '@speckle/shared' import { expect } from 'chai' import cryptoRandomString from 'crypto-random-string' -import { isUndefined } from 'lodash' -const grantStreamPermissions = grantStreamPermissionsFactory({ db }) - -describe('Workspaces Roles GQL', () => { +describe('Workspaces Roles/Seats GQL', () => { let apollo: TestApolloServer const serverAdminUser: BasicTestUser = { @@ -64,6 +77,75 @@ describe('Workspaces Roles GQL', () => { }) }) + const getWorkspaceProjects = async (params: { + user: BasicTestUser + workspace: BasicTestWorkspace + }) => { + const res = await apollo.execute( + GetUserWorkspaceProjectsWithAccessChecksDocument, + { + id: params.workspace.id + }, + { authUserId: params.user.id, assertNoErrors: true } + ) + + const projects = res.data?.workspace.projects.items || [] + expect(res.data?.workspace, 'Could not retrieve workspace for user').to.be.ok + + return { + projects, + workspace: res.data!.workspace, + checkProject: (project: BasicTestStream) => { + return projectImplicitRoleCheck(projects.find((p) => p.id === project.id)) + }, + checkAllProjects: (check: (project: ProjectImplicitRoleCheck) => boolean) => { + return projects.map(projectImplicitRoleCheck).every(check) + } + } + } + + const getUserProjects = async (params: { user: BasicTestUser }) => { + const res = await apollo.execute( + GetUserProjectsWithAccessChecksDocument, + { + filter: { + includeImplicitAccess: true + } + }, + { authUserId: params.user.id, assertNoErrors: true } + ) + + const projects = res.data?.activeUser?.projects.items || [] + + return { + projects, + checkProject: (project: BasicTestStream) => { + return projectImplicitRoleCheck(projects.find((p) => p.id === project.id)) + }, + checkAllProjects: (check: (project: ProjectImplicitRoleCheck) => boolean) => { + return projects.map(projectImplicitRoleCheck).every(check) + } + } + } + + const getUserWorkspace = async (params: { + user: BasicTestUser + workspace: BasicTestWorkspace + }) => { + const res = await apollo.execute( + GetUserWorkspaceAccessDocument, + { + id: params.workspace.id + }, + { authUserId: params.user.id } + ) + const workspace = res.data?.workspace + + return { + workspace + } + } + describe('single role changes in a workspace without projects', () => { const workspace: BasicTestWorkspace = { id: '', @@ -73,18 +155,27 @@ describe('Workspaces Roles GQL', () => { } before(async () => { - await createTestWorkspace(workspace, serverAdminUser) + await createTestWorkspace(workspace, serverAdminUser, { + addPlan: { + name: 'team', + status: 'valid' + } + }) }) describe('update workspace role', () => { after(async () => { - await apollo.execute(UpdateWorkspaceRoleDocument, { - input: { - userId: serverMemberUser.id, - workspaceId: workspace.id, - role: null - } - }) + await apollo.execute( + UpdateWorkspaceRoleDocument, + { + input: { + userId: serverMemberUser.id, + workspaceId: workspace.id, + role: null + } + }, + { assertNoErrors: true } + ) }) it('should create a role if none exists', async () => { @@ -150,13 +241,17 @@ describe('Workspaces Roles GQL', () => { describe('delete workspace role', () => { before(async () => { - await apollo.execute(UpdateWorkspaceRoleDocument, { - input: { - userId: serverMemberUser.id, - workspaceId: workspace.id, - role: Roles.Workspace.Member - } - }) + await apollo.execute( + UpdateWorkspaceRoleDocument, + { + input: { + userId: serverMemberUser.id, + workspaceId: workspace.id, + role: Roles.Workspace.Member + } + }, + { assertNoErrors: true } + ) }) it('should delete the specified role', async () => { @@ -188,7 +283,8 @@ describe('Workspaces Roles GQL', () => { }) }) - describe('single role changes in a workspace with projects', () => { + // TODO: Viewer vs Editor + describe('in a workspace with projects', () => { const workspace: BasicTestWorkspace = { id: '', ownerId: '', @@ -214,6 +310,18 @@ describe('Workspaces Roles GQL', () => { email: 'john-guest-speckle@example.org' } + const workspaceMemberViewerUser: BasicTestUser = { + id: '', + name: 'John "Member" Viewer Speckel', + email: 'john-member-speckle-viewer@example.org' + } + + const workspaceGuestViewerUser: BasicTestUser = { + id: '', + name: 'John "Middle Child" Viewer Speckle', + email: 'john-guest-speckle-viewer@example.org' + } + const workspaceProjectA: BasicTestStream = { id: '', ownerId: '', @@ -253,17 +361,53 @@ describe('Workspaces Roles GQL', () => { await createTestUsers([ workspaceAdminUser, workspaceMemberUser, - workspaceGuestUser + workspaceGuestUser, + workspaceMemberViewerUser, + workspaceGuestViewerUser ]) }) beforeEach(async () => { - await createTestWorkspace(workspace, serverAdminUser) + await createTestWorkspace(workspace, serverAdminUser, { + addPlan: { + name: 'team', + status: 'valid' + } + }) + await Promise.all([ - assignToWorkspace(workspace, workspaceAdminUser, Roles.Workspace.Admin), - assignToWorkspace(workspace, workspaceMemberUser, Roles.Workspace.Member), - assignToWorkspace(workspace, workspaceGuestUser, Roles.Workspace.Guest) + assignToWorkspace( + workspace, + workspaceAdminUser, + Roles.Workspace.Admin, + WorkspaceSeatType.Editor + ), + assignToWorkspace( + workspace, + workspaceMemberUser, + Roles.Workspace.Member, + WorkspaceSeatType.Editor + ), + assignToWorkspace( + workspace, + workspaceGuestUser, + Roles.Workspace.Guest, + WorkspaceSeatType.Editor + ), + assignToWorkspace( + workspace, + workspaceMemberViewerUser, + Roles.Workspace.Member, + WorkspaceSeatType.Viewer + ), + assignToWorkspace( + workspace, + workspaceGuestViewerUser, + Roles.Workspace.Guest, + WorkspaceSeatType.Viewer + ) ]) + for (const project of workspaceProjects) { project.workspaceId = workspace.id await createTestStream(project, serverAdminUser) @@ -272,478 +416,797 @@ describe('Workspaces Roles GQL', () => { /** * Initial workspace roles: * - * workspaceAdminUser Admin - * workspaceMemberUser Member - * workspaceGuestUser Guest + * workspaceAdminUser Admin (Editor) + * workspaceMemberUser Member (Editor) + * workspaceGuestUser Guest (Editor) + * workspaceMemberViewerUser Member (Viewer) + * workspaceGuestViewerUser Guest (Viewer) * - * Initial workspace project roles: + * Initial explicit workspace project roles: * - * | | Project A | Project B | Project C | Project D | - * |---------------------|-------------|-------------|-----------|-----------| - * | workspaceAdminUser | Owner | Owner | Owner | Owner | - * | workspaceMemberUser | Owner | Contributor | Reviewer | None | - * | workspaceGuestUser | Contributor | Reviewer | None | None | + * | | Project A | Project B | Project C | Project D | + * |---------------------------|-------------|-------------|-----------|-----------| + * | workspaceAdminUser | Owner | None | None | None | + * | workspaceMemberUser | Owner | Contributor | Reviewer | None | + * | workspaceGuestUser | Contributor | Reviewer | None | None | + * | workspaceMemberViewerUser | Reviewer | None | None | None | + * | workspaceGuestViewerUser | None | Reviewer | None | None | */ await Promise.all([ // A - grantStreamPermissions({ - streamId: workspaceProjectA.id, - userId: workspaceAdminUser.id, - role: Roles.Stream.Owner - }), - grantStreamPermissions({ - streamId: workspaceProjectA.id, - userId: workspaceMemberUser.id, - role: Roles.Stream.Owner - }), - grantStreamPermissions({ - streamId: workspaceProjectA.id, - userId: workspaceGuestUser.id, - role: Roles.Stream.Contributor - }), + addToStream(workspaceProjectA, workspaceAdminUser, Roles.Stream.Owner), + addToStream(workspaceProjectA, workspaceMemberUser, Roles.Stream.Owner), + addToStream(workspaceProjectA, workspaceGuestUser, Roles.Stream.Contributor), + addToStream( + workspaceProjectA, + workspaceMemberViewerUser, + Roles.Stream.Reviewer + ), // B - grantStreamPermissions({ - streamId: workspaceProjectB.id, - userId: workspaceAdminUser.id, - role: Roles.Stream.Owner - }), - grantStreamPermissions({ - streamId: workspaceProjectB.id, - userId: workspaceMemberUser.id, - role: Roles.Stream.Contributor - }), - grantStreamPermissions({ - streamId: workspaceProjectB.id, - userId: workspaceGuestUser.id, - role: Roles.Stream.Reviewer - }), + addToStream(workspaceProjectB, workspaceMemberUser, Roles.Stream.Contributor), + addToStream(workspaceProjectB, workspaceGuestUser, Roles.Stream.Reviewer), + addToStream(workspaceProjectB, workspaceGuestViewerUser, Roles.Stream.Reviewer), // C - grantStreamPermissions({ - streamId: workspaceProjectC.id, - userId: workspaceAdminUser.id, - role: Roles.Stream.Owner - }), - grantStreamPermissions({ - streamId: workspaceProjectC.id, - userId: workspaceMemberUser.id, - role: Roles.Stream.Reviewer - }), - // D - grantStreamPermissions({ - streamId: workspaceProjectD.id, - userId: workspaceAdminUser.id, - role: Roles.Stream.Owner - }) + addToStream(workspaceProjectC, workspaceMemberUser, Roles.Stream.Reviewer) ]) }) afterEach(async () => { - await truncateTables(['workspaces', 'streams']) + await truncateTables([ + Workspaces.name, + Streams.name, + WorkspaceAcl.name, + WorkspaceSeats.name + ]) }) - describe('when changing workspace admin', () => { - describe('to workspace member', () => { - beforeEach(async () => { - await apollo.execute(UpdateWorkspaceRoleDocument, { - input: { - userId: workspaceAdminUser.id, - workspaceId: workspace.id, - role: Roles.Workspace.Member - } - }) + const getProjects = async (params: { user: BasicTestUser }) => + getWorkspaceProjects({ user: params.user, workspace }) + + describe('retrieving projects', () => { + it('workspaceAdminUser is implicit owner of all of them and explicit owner in one', async () => { + const { projects, checkProject, checkAllProjects } = await getProjects({ + user: workspaceAdminUser }) - it('should grant default project role for all workspace projects', async () => { - const res = await apollo.execute(GetWorkspaceProjectsDocument, { - id: workspace.id - }) - - const projects = res.data?.workspace.projects.items - - expect(res).to.not.haveGraphQLErrors() - expect(projects).to.exist - expect( - projects?.every((project) => { - const team = project.team - const role = team.find((acl) => acl.id === workspaceAdminUser.id) - return role?.role === Roles.Stream.Reviewer - }) - ).to.be.true - }) + expect(projects.length).to.eq(4) + expect(checkAllProjects((p) => p.isOwner)).to.be.ok + expect(checkProject(workspaceProjectA).isExplicitOwner).to.be.ok + expect(checkProject(workspaceProjectB).hasExplicitRole).to.be.not.ok + expect(checkProject(workspaceProjectC).hasExplicitRole).to.be.not.ok + expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok }) - describe('to workspace guest', () => { - beforeEach(async () => { - await apollo.execute(UpdateWorkspaceRoleDocument, { - input: { - userId: workspaceAdminUser.id, - workspaceId: workspace.id, - role: Roles.Workspace.Guest - } - }) + it('workspaceMemberUser is implicit reviewer in all of them, and also has explicit roles in some', async () => { + const { projects, checkAllProjects, checkProject } = await getProjects({ + user: workspaceMemberUser }) - it('should drop all workspace project roles', async () => { - const res = await apollo.execute(GetWorkspaceProjectsDocument, { - id: workspace.id - }) + expect(projects.length).to.eq(4) + expect(checkAllProjects((p) => p.isReviewer)).to.be.ok + expect(checkProject(workspaceProjectA).isExplicitOwner).to.be.ok + expect(checkProject(workspaceProjectB).isExplicitContributor).to.be.ok + expect(checkProject(workspaceProjectC).isExplicitReviewer).to.be.ok + expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok + }) - const projects = res.data?.workspace.projects.items - - expect(res).to.not.haveGraphQLErrors() - expect(projects).to.exist - expect( - projects?.every((project) => { - const team = project.team - const role = team.find((acl) => acl.id === workspaceAdminUser.id) - return isUndefined(role) - }) - ).to.be.true + it('workspaceGuestUser only has explicit roles in 2 projects', async () => { + const { projects, checkProject } = await getProjects({ + user: workspaceGuestUser }) + + expect(projects.length).to.eq(2) + expect(checkProject(workspaceProjectA).isExplicitContributor).to.be.ok + expect(checkProject(workspaceProjectB).isExplicitReviewer).to.be.ok + expect(checkProject(workspaceProjectC).hasExplicitRole).to.be.not.ok + expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok + }) + + it('workspaceMemberViewerUser is only explicit reviewer in 1 project, and has implicit roles elsewhere', async () => { + const { projects, checkAllProjects, checkProject } = await getProjects({ + user: workspaceMemberViewerUser + }) + expect(projects.length).to.eq(4) + expect(checkAllProjects((p) => p.isReviewer)).to.be.ok + expect(checkProject(workspaceProjectA).isExplicitReviewer).to.be.ok + expect(checkProject(workspaceProjectB).hasExplicitRole).to.be.not.ok + expect(checkProject(workspaceProjectC).hasExplicitRole).to.be.not.ok + expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok + }) + + it('workspaceGuestViewerUser is only explicit reviewer in 1 project', async () => { + const { projects, checkProject } = await getProjects({ + user: workspaceGuestViewerUser + }) + + expect(projects.length).to.eq(1) + expect(checkProject(workspaceProjectB).isExplicitReviewer).to.be.ok + expect(checkProject(workspaceProjectA).hasExplicitRole).to.be.not.ok + expect(checkProject(workspaceProjectC).hasExplicitRole).to.be.not.ok + expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok }) }) - describe('when changing workspace member', () => { - describe('to workspace admin', () => { - beforeEach(async () => { - await apollo.execute(UpdateWorkspaceRoleDocument, { + describe('doing single seat type changes', () => { + it('cant change workspace admin to viewer', async () => { + const res = await apollo.execute(UpdateWorkspaceSeatTypeDocument, { + input: { + userId: workspaceAdminUser.id, + workspaceId: workspace.id, + seatType: WorkspaceSeatType.Viewer + } + }) + + expect(res).to.haveGraphQLErrors('cannot have a seat of type') + }) + + it('changing member editor to viewer, should downgrade all explicit roles to reviewer', async () => { + await apollo.execute( + UpdateWorkspaceSeatTypeDocument, + { input: { userId: workspaceMemberUser.id, workspaceId: workspace.id, - role: Roles.Workspace.Admin + seatType: WorkspaceSeatType.Viewer } - }) + }, + { assertNoErrors: true } + ) + + const { projects, checkProject } = await getProjects({ + user: workspaceMemberUser }) - it('should grant project owner role for all workspace projects', async () => { - const res = await apollo.execute(GetWorkspaceProjectsDocument, { - id: workspace.id - }) - - const projects = res.data?.workspace.projects.items - - expect(res).to.not.haveGraphQLErrors() - expect(projects).to.exist - expect( - projects?.every((project) => { - const team = project.team - const role = team.find((acl) => acl.id === workspaceMemberUser.id) - return role?.role === Roles.Stream.Owner - }) - ).to.be.true - }) + expect(projects.length).to.eq(4) + expect(checkProject(workspaceProjectA).isExplicitReviewer).to.be.ok + expect(checkProject(workspaceProjectB).isExplicitReviewer).to.be.ok + expect(checkProject(workspaceProjectC).isExplicitReviewer).to.be.ok + expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok }) - describe('to workspace guest', () => { - beforeEach(async () => { - await apollo.execute(UpdateWorkspaceRoleDocument, { + it('changing guest editor to viewer, should downgrade all explicit roles to reviewer', async () => { + await apollo.execute( + UpdateWorkspaceSeatTypeDocument, + { input: { - userId: workspaceMemberUser.id, + userId: workspaceGuestUser.id, workspaceId: workspace.id, - role: Roles.Workspace.Guest + seatType: WorkspaceSeatType.Viewer } - }) + }, + { assertNoErrors: true } + ) + + const { projects, checkProject } = await getProjects({ + user: workspaceGuestUser }) - it('should drop all workspace project roles', async () => { - const res = await apollo.execute(GetWorkspaceProjectsDocument, { - id: workspace.id - }) - - const projects = res.data?.workspace.projects.items - - expect(res).to.not.haveGraphQLErrors() - expect(projects).to.exist - expect( - projects?.every((project) => { - const team = project.team - const role = team.find((acl) => acl.id === workspaceMemberUser.id) - return isUndefined(role) - }) - ).to.be.true - }) + expect(projects.length).to.eq(2) + expect(checkProject(workspaceProjectA).isExplicitReviewer).to.be.ok + expect(checkProject(workspaceProjectB).isExplicitReviewer).to.be.ok }) }) - describe('when changing workspace guest', () => { - describe('to workspace admin', () => { - beforeEach(async () => { - await apollo.execute(UpdateWorkspaceRoleDocument, { - input: { - userId: workspaceGuestUser.id, - workspaceId: workspace.id, - role: Roles.Workspace.Admin - } + describe('doing single role changes', () => { + describe('when changing workspace admin', () => { + describe('to workspace member', () => { + beforeEach(async () => { + await apollo.execute( + UpdateWorkspaceRoleDocument, + { + input: { + userId: workspaceAdminUser.id, + workspaceId: workspace.id, + role: Roles.Workspace.Member + } + }, + { assertNoErrors: true } + ) + }) + + it('should still remain explicit owner and be implicit reviewer elsewhere', async () => { + const { projects, checkAllProjects, checkProject } = await getProjects({ + user: workspaceAdminUser + }) + + expect(projects.length).to.eq(4) + expect(checkProject(workspaceProjectA).isExplicitOwner).to.be.ok + expect(checkProject(workspaceProjectB).hasExplicitRole).to.be.not.ok + expect(checkProject(workspaceProjectC).hasExplicitRole).to.be.not.ok + expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok + expect(checkAllProjects((p) => p.isReviewer)).to.be.ok }) }) - it('should grant project owner role for all workspace projects', async () => { - const res = await apollo.execute(GetWorkspaceProjectsDocument, { - id: workspace.id + describe('to workspace guest', () => { + beforeEach(async () => { + await apollo.execute( + UpdateWorkspaceRoleDocument, + { + input: { + userId: workspaceAdminUser.id, + workspaceId: workspace.id, + role: Roles.Workspace.Guest + } + }, + { assertNoErrors: true } + ) }) - const projects = res.data?.workspace.projects.items - - expect(res).to.not.haveGraphQLErrors() - expect(projects).to.exist - expect( - projects?.every((project) => { - const team = project.team - const role = team.find((acl) => acl.id === workspaceGuestUser.id) - return role?.role === Roles.Stream.Owner + it('should only have 1 project access, and not owner, but contributor', async () => { + const { projects, checkProject } = await getProjects({ + user: workspaceAdminUser }) - ).to.be.true + + expect(projects.length).to.eq(1) + expect(checkProject(workspaceProjectA).isOwner).to.not.be.ok + expect(checkProject(workspaceProjectA).isExplicitContributor).to.be.ok + expect(checkProject(workspaceProjectB).hasExplicitRole).to.be.not.ok + expect(checkProject(workspaceProjectC).hasExplicitRole).to.be.not.ok + expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok + }) }) }) - describe('to workspace member', () => { - beforeEach(async () => { - await apollo.execute(UpdateWorkspaceRoleDocument, { - input: { - userId: workspaceGuestUser.id, - workspaceId: workspace.id, - role: Roles.Workspace.Member - } + describe('when changing workspace member', () => { + describe('to workspace admin', () => { + beforeEach(async () => { + await apollo.execute( + UpdateWorkspaceRoleDocument, + { + input: { + userId: workspaceMemberUser.id, + workspaceId: workspace.id, + role: Roles.Workspace.Admin + } + }, + { assertNoErrors: true } + ) + }) + + it('should get implicit owner role everywhere and explicit upgraded to owner', async () => { + const { projects, checkProject, checkAllProjects } = await getProjects({ + user: workspaceMemberUser + }) + + expect(projects.length).to.eq(4) + expect(checkAllProjects((p) => p.isOwner)).to.be.ok + expect(checkProject(workspaceProjectA).isExplicitOwner).to.be.ok + expect(checkProject(workspaceProjectB).isExplicitOwner).to.be.ok + expect(checkProject(workspaceProjectC).isExplicitOwner).to.be.ok + expect(checkProject(workspaceProjectD).hasExplicitRole).to.not.be.ok }) }) - it('should grant default project role for all workspace projects', async () => { - const res = await apollo.execute(GetWorkspaceProjectsDocument, { - id: workspace.id + describe('to workspace guest', () => { + beforeEach(async () => { + await apollo.execute( + UpdateWorkspaceRoleDocument, + { + input: { + userId: workspaceMemberUser.id, + workspaceId: workspace.id, + role: Roles.Workspace.Guest + } + }, + { assertNoErrors: true } + ) }) - const projects = res.data?.workspace.projects.items - - expect(res).to.not.haveGraphQLErrors() - expect(projects).to.exist - expect( - projects?.every((project) => { - const team = project.team - const role = team.find((acl) => acl.id === workspaceGuestUser.id) - // TODO: This is a workspace setting - return role?.role === Roles.Stream.Reviewer + it('no implicit access and all explicit downgraded to contributor or less', async () => { + const { projects, checkProject } = await getProjects({ + user: workspaceMemberUser }) - ).to.be.true + + expect(projects.length).to.eq(3) + expect(checkProject(workspaceProjectA).isExplicitContributor).to.be.ok + expect(checkProject(workspaceProjectB).isExplicitContributor).to.be.ok + expect(checkProject(workspaceProjectC).isExplicitReviewer).to.be.ok + expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok + }) + }) + }) + + describe('when changing workspace guest', () => { + describe('to workspace admin', () => { + beforeEach(async () => { + await apollo.execute( + UpdateWorkspaceRoleDocument, + { + input: { + userId: workspaceGuestUser.id, + workspaceId: workspace.id, + role: Roles.Workspace.Admin + } + }, + { assertNoErrors: true } + ) + }) + + it('should upgrade explicit role to owner, and have implicit owner everywhere', async () => { + const { projects, checkProject, checkAllProjects } = await getProjects({ + user: workspaceGuestUser + }) + + expect(projects.length).to.eq(4) + expect(checkAllProjects((p) => p.isOwner)).to.be.ok + expect(checkProject(workspaceProjectA).isExplicitOwner).to.be.ok + expect(checkProject(workspaceProjectB).isExplicitOwner).to.be.ok + expect(checkProject(workspaceProjectC).hasExplicitRole).to.not.be.ok + expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok + }) + }) + + describe('to workspace member', () => { + beforeEach(async () => { + await apollo.execute( + UpdateWorkspaceRoleDocument, + { + input: { + userId: workspaceGuestUser.id, + workspaceId: workspace.id, + role: Roles.Workspace.Member + } + }, + { assertNoErrors: true } + ) + }) + + it('should retain same explicit access and get full implicit acccess', async () => { + const { projects, checkProject, checkAllProjects } = await getProjects({ + user: workspaceGuestUser + }) + + expect(projects.length).to.eq(4) + expect(checkAllProjects((p) => p.isReviewer)).to.be.ok + expect(checkProject(workspaceProjectA).isExplicitContributor).to.be.ok + expect(checkProject(workspaceProjectB).isExplicitReviewer).to.be.ok + expect(checkProject(workspaceProjectC).hasExplicitRole).to.be.not.ok + expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok + }) + }) + }) + + describe('when changing workspace member viewer', () => { + describe('to workspace admin', () => { + beforeEach(async () => { + await apollo.execute( + UpdateWorkspaceRoleDocument, + { + input: { + userId: workspaceMemberViewerUser.id, + workspaceId: workspace.id, + role: Roles.Workspace.Admin + } + }, + { assertNoErrors: true } + ) + }) + + it('should get editor seat, implicit owner role everywhere and explicit upgraded to owner', async () => { + const { workspace, projects, checkProject, checkAllProjects } = + await getProjects({ + user: workspaceMemberViewerUser + }) + + expect(workspace.seatType).to.eq(WorkspaceSeatType.Editor) + expect(projects.length).to.eq(4) + expect(checkAllProjects((p) => p.isOwner)).to.be.ok + expect(checkProject(workspaceProjectA).isExplicitOwner).to.be.ok + expect(checkProject(workspaceProjectB).hasExplicitRole).to.not.be.ok + expect(checkProject(workspaceProjectC).hasExplicitRole).to.not.be.ok + expect(checkProject(workspaceProjectD).hasExplicitRole).to.not.be.ok + }) + }) + + describe('to workspace guest', () => { + beforeEach(async () => { + await apollo.execute( + UpdateWorkspaceRoleDocument, + { + input: { + userId: workspaceMemberViewerUser.id, + workspaceId: workspace.id, + role: Roles.Workspace.Guest + } + }, + { assertNoErrors: true } + ) + }) + + it('retain viewer seat, no implicit access and all explicit at reviewer or less', async () => { + const { projects, checkProject, workspace } = await getProjects({ + user: workspaceMemberViewerUser + }) + + expect(workspace.seatType).to.eq(WorkspaceSeatType.Viewer) + expect(projects.length).to.eq(1) + expect(checkProject(workspaceProjectA).isExplicitReviewer).to.be.ok + }) + }) + }) + + describe('when changing workspace guest viewer', () => { + describe('to workspace admin', () => { + beforeEach(async () => { + await apollo.execute( + UpdateWorkspaceRoleDocument, + { + input: { + userId: workspaceGuestViewerUser.id, + workspaceId: workspace.id, + role: Roles.Workspace.Admin + } + }, + { assertNoErrors: true } + ) + }) + + it('should upgrade seatType to editor, explicit role to owner, and have implicit owner everywhere', async () => { + const { workspace, projects, checkProject, checkAllProjects } = + await getProjects({ + user: workspaceGuestViewerUser + }) + + expect(workspace.seatType).to.eq(WorkspaceSeatType.Editor) + expect(projects.length).to.eq(4) + expect(checkAllProjects((p) => p.isOwner)).to.be.ok + expect(checkProject(workspaceProjectA).hasExplicitRole).to.not.be.ok + expect(checkProject(workspaceProjectB).isExplicitOwner).to.be.ok + expect(checkProject(workspaceProjectC).hasExplicitRole).to.not.be.ok + expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok + }) + }) + + describe('to workspace member', () => { + beforeEach(async () => { + await apollo.execute( + UpdateWorkspaceRoleDocument, + { + input: { + userId: workspaceGuestViewerUser.id, + workspaceId: workspace.id, + role: Roles.Workspace.Member + } + }, + { assertNoErrors: true } + ) + }) + + it('should retain viewer seat, same explicit access and get full implicit acccess', async () => { + const { workspace, projects, checkProject, checkAllProjects } = + await getProjects({ + user: workspaceGuestViewerUser + }) + + expect(workspace.seatType).to.eq(WorkspaceSeatType.Viewer) + expect(projects.length).to.eq(4) + expect(checkAllProjects((p) => p.isReviewer)).to.be.ok + expect(checkProject(workspaceProjectA).hasExplicitRole).to.be.not.ok + expect(checkProject(workspaceProjectB).isExplicitReviewer).to.be.ok + expect(checkProject(workspaceProjectC).hasExplicitRole).to.be.not.ok + expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok + }) }) }) }) }) - describe('composite role changes in a workspace with projects', () => { - let workspaceMemberApollo: TestApolloServer - - const workspace: BasicTestWorkspace = { + describe('doing composite role/seat changes', () => { + const testWorkspace: BasicTestWorkspace = { id: '', ownerId: '', - slug: cryptoRandomString({ length: 10 }), - name: 'Test Workspace w/ Projects' + slug: '', + name: 'Test Composite Role Change Workspace' } - const workspaceProject: BasicTestStream = { + const workspaceAdminUser: BasicTestUser = { id: '', - ownerId: '', - name: 'Test Project', - isPublic: true + name: 'Composite John "Owner" Specke', + email: 'composite-john-owner-speckle@example.org' + } + + const workspaceMemberUser: BasicTestUser = { + id: '', + name: 'Composite John "Member" Speckel', + email: 'composite-john-member-speckle@example.org' } before(async () => { - const token = await createAuthTokenForUser(serverMemberUser.id, AllScopes) - workspaceMemberApollo = await testApolloServer({ - context: await createTestContext({ - auth: true, - userId: serverMemberUser.id, - token, - role: serverMemberUser.role, - scopes: AllScopes - }) - }) + await createTestUsers([workspaceAdminUser, workspaceMemberUser]) }) beforeEach(async () => { - await createTestWorkspace(workspace, serverAdminUser) - workspaceProject.workspaceId = workspace.id - await createTestStream(workspaceProject, serverAdminUser) + await createTestWorkspace(testWorkspace, serverAdminUser, { + addPlan: { + name: 'team', + status: 'valid' + } + }) + + await assignToWorkspace( + testWorkspace, + workspaceAdminUser, + Roles.Workspace.Admin, + WorkspaceSeatType.Editor + ) + await assignToWorkspace( + testWorkspace, + workspaceMemberUser, + Roles.Workspace.Member, + WorkspaceSeatType.Editor + ) }) afterEach(async () => { - await truncateTables(['workspaces', 'streams']) + await truncateTables([ + Workspaces.name, + Streams.name, + WorkspaceAcl.name, + WorkspaceSeats.name + ]) }) - describe('when leaving the workspace as the last owner of a workspace project', () => { + it('downgrading admin->guest if last owner, sets new owner from workspace admins', async () => { // User Workspace Role Project Role - // serverAdminUser Admin Reviewer - // serverMemberUser Admin Owner + // serverAdminUser Admin None + // workspaceAdminUser Admin Owner // - // Action: `serverMemberUser` leaves workspace + // Action: `workspaceAdminUser` downgraded to workspace guest - beforeEach(async () => { - await assignToWorkspace(workspace, serverMemberUser, Roles.Workspace.Admin) - await grantStreamPermissions({ - streamId: workspaceProject.id, - userId: serverAdminUser.id, - role: Roles.Stream.Reviewer - }) + const project: BasicTestStream = { + id: '', + ownerId: '', + name: 'Test Composite Project', + isPublic: false, + workspaceId: testWorkspace.id + } + await createTestStream(project, workspaceAdminUser) + const apollo = await testApolloServer({ + authUserId: serverAdminUser.id }) - it('should throw and preserve all roles', async () => { - const res = await workspaceMemberApollo.execute( - ActiveUserLeaveWorkspaceDocument, - { id: workspace.id } - ) - - const { data: workspaceTeamData } = await apollo.execute( - GetWorkspaceTeamDocument, - { workspaceId: workspace.id } - ) - const { data: workspaceProjectsData } = await apollo.execute( - GetWorkspaceProjectsDocument, - { id: workspace.id } - ) - - const teamRoles = workspaceTeamData?.workspace.team.items - const projectRoles = workspaceProjectsData?.workspace.projects.items[0].team - - expect(res).to.haveGraphQLErrors('Could not revoke permissions for last admin') - expect(teamRoles).to.exist - expect(teamRoles?.some((role) => role.id === serverMemberUser.id)).to.be.true - expect(projectRoles).to.exist - expect(projectRoles?.some((role) => role.id === serverMemberUser.id)).to.be.true + const remove = await apollo.execute(UpdateWorkspaceRoleDocument, { + input: { + userId: workspaceAdminUser.id, + role: Roles.Workspace.Guest, + workspaceId: testWorkspace.id + } }) + expect(remove).to.not.haveGraphQLErrors() + + const { workspace, checkProject } = await getWorkspaceProjects({ + user: workspaceAdminUser, + workspace: testWorkspace + }) + expect(workspace?.role).to.eq(Roles.Workspace.Guest) + expect(checkProject(project).isExplicitContributor).to.be.ok + + const { checkProject: checkProjectForAdmin } = await getUserProjects({ + user: serverAdminUser + }) + expect(checkProjectForAdmin(project).isExplicitOwner).to.be.ok }) - describe('when removing a workspace member that is the last owner of a workspace project', () => { + it('downgrading member to viewer if last owner, sets new owner from workspace admins', async () => { // User Workspace Role Project Role - // serverAdminUser Admin Reviewer - // serverMemberUser Admin Owner + // workspaceAdminUser Admin None + // workspaceMemberUser Member Owner // - // Action: `serverAdminUser` removes `serverMemberUser` from the workspace + // Action: `workspaceAdminUser` downgraded to workspace guest - beforeEach(async () => { - await assignToWorkspace(workspace, serverMemberUser, Roles.Workspace.Admin) - await grantStreamPermissions({ - streamId: workspaceProject.id, - userId: serverAdminUser.id, - role: Roles.Stream.Reviewer - }) + // ensure serverAdmin is no longer admin, so there's only 1 - workspaceAdmin + await unassignFromWorkspace(testWorkspace, serverAdminUser) + + const project: BasicTestStream = { + id: '', + ownerId: '', + name: 'Test Composite Project', + isPublic: false, + workspaceId: testWorkspace.id + } + await createTestStream(project, workspaceMemberUser) + const apollo = await testApolloServer({ + authUserId: workspaceAdminUser.id }) - it('should throw and preserve all roles', async () => { - const res = await apollo.execute(UpdateWorkspaceRoleDocument, { - input: { - userId: serverMemberUser.id, - role: null, - workspaceId: workspace.id - } - }) - - const { data: workspaceTeamData } = await apollo.execute( - GetWorkspaceTeamDocument, - { workspaceId: workspace.id } - ) - const { data: workspaceProjectsData } = await apollo.execute( - GetWorkspaceProjectsDocument, - { id: workspace.id } - ) - - const teamRoles = workspaceTeamData?.workspace.team.items - const projectRoles = workspaceProjectsData?.workspace.projects.items[0].team - - expect(res).to.haveGraphQLErrors('Could not revoke permissions for last admin') - expect(teamRoles).to.exist - expect(teamRoles?.some((role) => role.id === serverMemberUser.id)).to.be.true - expect(projectRoles).to.exist - expect(projectRoles?.some((role) => role.id === serverMemberUser.id)).to.be.true + const downgrade = await apollo.execute(UpdateWorkspaceSeatTypeDocument, { + input: { + userId: workspaceMemberUser.id, + workspaceId: testWorkspace.id, + seatType: WorkspaceSeatType.Viewer + } }) + expect(downgrade).to.not.haveGraphQLErrors() + + const { workspace, checkProject } = await getWorkspaceProjects({ + user: workspaceMemberUser, + workspace: testWorkspace + }) + expect(workspace?.role).to.eq(Roles.Workspace.Member) + expect(workspace?.seatType).to.eq(WorkspaceSeatType.Viewer) + expect(checkProject(project).isExplicitReviewer).to.be.ok + + const { checkProject: checkProjectForAdmin } = await getUserProjects({ + user: workspaceAdminUser + }) + expect(checkProjectForAdmin(project).isExplicitOwner).to.be.ok }) - describe('when leaving a workspace without any project owner roles', () => { + it('leaving workspace as last owner of a workspace, sets new owner from workspace admins', async () => { // User Workspace Role Project Role - // serverAdminUser Admin Owner - // serverMemberUser Member Reviewer + // workspaceAdminUser Admin None + // workspaceMemberUser Member Owner // - // Action: `serverMemberUser` leaves workspace + // Action: `workspaceMemberUser` leaves workspace - beforeEach(async () => { - await assignToWorkspace(workspace, serverMemberUser, Roles.Workspace.Member) - await grantStreamPermissions({ - streamId: workspaceProject.id, - userId: serverMemberUser.id, - role: Roles.Stream.Reviewer - }) + // ensure serverAdmin is no longer admin, so there's only 1 - workspaceAdmin + await unassignFromWorkspace(testWorkspace, serverAdminUser) + + const project: BasicTestStream = { + id: '', + ownerId: '', + name: 'Test Leave Project', + isPublic: false, + workspaceId: testWorkspace.id + } + await createTestStream(project, workspaceMemberUser) + const apollo = await testApolloServer({ + authUserId: workspaceMemberUser.id }) - it('should remove all workspace and project roles for user', async () => { - const res = await workspaceMemberApollo.execute( - ActiveUserLeaveWorkspaceDocument, - { id: workspace.id } - ) - - const { data: workspaceTeamData } = await apollo.execute( - GetWorkspaceTeamDocument, - { workspaceId: workspace.id } - ) - const { data: workspaceProjectsData } = await apollo.execute( - GetWorkspaceProjectsDocument, - { id: workspace.id } - ) - - const teamRoles = workspaceTeamData?.workspace.team.items - const projectRoles = workspaceProjectsData?.workspace.projects.items[0].team - - expect(res).to.not.haveGraphQLErrors() - expect(teamRoles).to.exist - expect(teamRoles?.some((role) => role.id === serverMemberUser.id)).to.be.false - expect(projectRoles).to.exist - expect(projectRoles?.some((role) => role.id === serverMemberUser.id)).to.be - .false + const leave = await apollo.execute(ActiveUserLeaveWorkspaceDocument, { + id: testWorkspace.id }) + expect(leave).to.not.haveGraphQLErrors() + + const { workspace } = await getUserWorkspace({ + user: workspaceMemberUser, + workspace: testWorkspace + }) + expect(workspace?.role).to.not.be.ok + + const { checkProject } = await getUserProjects({ + user: workspaceMemberUser + }) + expect(checkProject(project).hasExplicitRole).to.be.not.ok + + const { checkProject: checkProjectForAdmin } = await getUserProjects({ + user: workspaceAdminUser + }) + expect(checkProjectForAdmin(project).isExplicitOwner).to.be.ok }) - describe('when removing a workspace member that has no workspace project owner roles', () => { + it('leaving workspace w/o owner roles works fine and removes all roles', async () => { // User Workspace Role Project Role - // serverAdminUser Admin Owner - // serverMemberUser Member Reviewer + // workspaceAdminUser Admin Owner + // workspaceMemberUser Member Reviewer // - // Action: `serverAdminUser` removes `serverMemberUser` from the workspace + // Action: `workspaceMemberUser` leaves workspace - beforeEach(async () => { - await assignToWorkspace(workspace, serverMemberUser, Roles.Workspace.Member) - await grantStreamPermissions({ - streamId: workspaceProject.id, - userId: serverMemberUser.id, - role: Roles.Stream.Reviewer - }) + const project: BasicTestStream = { + id: '', + ownerId: '', + name: 'Test Leave Project', + isPublic: false, + workspaceId: testWorkspace.id + } + await createTestStream(project, workspaceAdminUser) + await addToStream(project, workspaceMemberUser, Roles.Stream.Reviewer) + + const apollo = await testApolloServer({ + authUserId: workspaceMemberUser.id }) - it('should remove all workspace and project roles for removed member', async () => { - const res = await apollo.execute(UpdateWorkspaceRoleDocument, { - input: { - userId: serverMemberUser.id, - role: null, - workspaceId: workspace.id - } - }) - - const { data: workspaceTeamData } = await apollo.execute( - GetWorkspaceTeamDocument, - { workspaceId: workspace.id } - ) - const { data: workspaceProjectsData } = await apollo.execute( - GetWorkspaceProjectsDocument, - { id: workspace.id } - ) - - const teamRoles = workspaceTeamData?.workspace.team.items - const projectRoles = workspaceProjectsData?.workspace.projects.items[0].team - - expect(res).to.not.haveGraphQLErrors() - expect(teamRoles).to.exist - expect(teamRoles?.some((role) => role.id === serverMemberUser.id)).to.be.false - expect(projectRoles).to.exist - expect(projectRoles?.some((role) => role.id === serverMemberUser.id)).to.be - .false + const leave = await apollo.execute(ActiveUserLeaveWorkspaceDocument, { + id: testWorkspace.id }) + expect(leave).to.not.haveGraphQLErrors() + expect(leave.data?.workspaceMutations.leave).to.be.ok + + const { checkProject } = await getUserProjects({ + user: workspaceMemberUser + }) + expect(checkProject(project).hasExplicitRole).to.be.not.ok + + const { workspace } = await getUserWorkspace({ + user: workspaceMemberUser, + workspace: testWorkspace + }) + expect(workspace?.role).to.be.not.ok + }) + + it('removing a workspace member that is the last owner of a workspace project sets new owner from workspace admins', async () => { + // User Workspace Role Project Role + // workspaceAdminUser Admin None + // workspaceMemberUser Member Owner + // + // Action: `workspaceAdminUser` removes `workspaceMemberUser` from the workspace + + // ensure serverAdmin is no longer admin, so there's only 1 - workspaceAdmin + await unassignFromWorkspace(testWorkspace, serverAdminUser) + + const project: BasicTestStream = { + id: '', + ownerId: '', + name: 'Test Remove Project', + isPublic: false, + workspaceId: testWorkspace.id + } + await createTestStream(project, workspaceMemberUser) + const apollo = await testApolloServer({ + authUserId: workspaceAdminUser.id + }) + + const remove = await apollo.execute(UpdateWorkspaceRoleDocument, { + input: { + userId: workspaceMemberUser.id, + role: null, + workspaceId: testWorkspace.id + } + }) + expect(remove).to.not.haveGraphQLErrors() + expect(remove.data?.workspaceMutations.updateRole).to.be.ok + + const { checkProject } = await getUserProjects({ + user: workspaceMemberUser + }) + expect(checkProject(project).hasExplicitRole).to.be.not.ok + + const { workspace } = await getUserWorkspace({ + user: workspaceMemberUser, + workspace: testWorkspace + }) + expect(workspace?.role).to.be.not.ok + + const { checkProject: checkProjectForAdmin } = await getUserProjects({ + user: workspaceAdminUser + }) + expect(checkProjectForAdmin(project).isExplicitOwner).to.be.ok + }) + + it('removing a workspace member that is not the last owner of a workspace project works fine and removes all roles', async () => { + // User Workspace Role Project Role + // workspaceAdminUser Admin Owner + // workspaceMemberUser Member Reviewer + // + // Action: `workspaceAdminUser` removes `workspaceMemberUser` from the workspace + + const project: BasicTestStream = { + id: '', + ownerId: '', + name: 'Test Remove Project', + isPublic: false, + workspaceId: testWorkspace.id + } + await createTestStream(project, workspaceAdminUser) + await addToStream(project, workspaceMemberUser, Roles.Stream.Reviewer) + + const apollo = await testApolloServer({ + authUserId: workspaceAdminUser.id + }) + + const remove = await apollo.execute(UpdateWorkspaceRoleDocument, { + input: { + userId: workspaceMemberUser.id, + role: null, + workspaceId: testWorkspace.id + } + }) + expect(remove).to.not.haveGraphQLErrors() + expect(remove.data?.workspaceMutations.updateRole).to.be.ok + + const { checkProject } = await getUserProjects({ + user: workspaceMemberUser + }) + expect(checkProject(project).hasExplicitRole).to.be.not.ok + + const { workspace } = await getUserWorkspace({ + user: workspaceMemberUser, + workspace: testWorkspace + }) + expect(workspace?.role).to.be.not.ok }) }) }) diff --git a/packages/server/modules/workspaces/tests/unit/events/eventListener.spec.ts b/packages/server/modules/workspaces/tests/unit/events/eventListener.spec.ts index 7de734acb..f02655073 100644 --- a/packages/server/modules/workspaces/tests/unit/events/eventListener.spec.ts +++ b/packages/server/modules/workspaces/tests/unit/events/eventListener.spec.ts @@ -1,13 +1,9 @@ import cryptoRandomString from 'crypto-random-string' import { Workspace, WorkspaceAcl } from '@/modules/workspacesCore/domain/types' -import { Roles, StreamRoles } from '@speckle/shared' +import { Roles } from '@speckle/shared' import { StreamAclRecord, StreamRecord } from '@/modules/core/helpers/types' -import { - onProjectCreatedFactory, - onWorkspaceRoleUpdatedFactory -} from '@/modules/workspaces/events/eventListener' +import { onProjectCreatedFactory } from '@/modules/workspaces/events/eventListener' import { expect } from 'chai' -import { chunk } from 'lodash' import { GetWorkspaceRolesAndSeats } from '@/modules/gatekeeper/domain/billing' describe('Event handlers', () => { @@ -89,132 +85,4 @@ describe('Event handlers', () => { expect(projectRoles.length).to.equal(2) }) }) - describe('onWorkspaceRoleUpdatedFactory creates a function, that', () => { - it('assigns no project roles if the role mapping returns null', async () => { - let isDeleteCalled = false - const fakeProject = { id: 'test' } as StreamRecord - - await onWorkspaceRoleUpdatedFactory({ - getWorkspaceWithPlan: async () => - ({ - id: 'fake' - } as Workspace & { plan: null }), - getWorkspaceRoleToDefaultProjectRoleMapping: async () => ({ - default: { - [Roles.Workspace.Admin]: Roles.Stream.Owner, - [Roles.Workspace.Member]: Roles.Stream.Contributor, - [Roles.Workspace.Guest]: null - }, - allowed: { - [Roles.Workspace.Admin]: [ - Roles.Stream.Owner, - Roles.Stream.Contributor, - Roles.Stream.Reviewer - ], - [Roles.Workspace.Member]: [ - Roles.Stream.Owner, - Roles.Stream.Contributor, - Roles.Stream.Reviewer - ], - [Roles.Workspace.Guest]: [Roles.Stream.Reviewer, Roles.Stream.Contributor] - } - }), - async *queryAllWorkspaceProjects() { - yield [fakeProject as StreamRecord] - }, - getStreamsCollaboratorCounts: async () => { - return {} - }, - setStreamCollaborator: async ({ role }) => { - if (!role) { - isDeleteCalled = true - } else { - expect.fail() - } - - return fakeProject - } - })({ - acl: { - role: Roles.Workspace.Guest, - userId: cryptoRandomString({ length: 10 }), - workspaceId: cryptoRandomString({ length: 10 }) - }, - updatedByUserId: cryptoRandomString({ length: 10 }) - }) - - expect(isDeleteCalled).to.be.true - }) - it('assigns the mapped projects roles to all queried project', async () => { - const projectIds = [ - cryptoRandomString({ length: 10 }), - cryptoRandomString({ length: 10 }), - cryptoRandomString({ length: 10 }), - cryptoRandomString({ length: 10 }) - ] - const userId = cryptoRandomString({ length: 10 }) - const projectRole = Roles.Stream.Reviewer - - const storedRoles: { userId: string; role: StreamRoles; projectId: string }[] = [] - let trackProjectUpdate: boolean | undefined = false - await onWorkspaceRoleUpdatedFactory({ - getWorkspaceWithPlan: async () => - ({ - id: 'fake' - } as Workspace & { plan: null }), - getWorkspaceRoleToDefaultProjectRoleMapping: async () => ({ - default: { - [Roles.Workspace.Admin]: Roles.Stream.Owner, - [Roles.Workspace.Member]: projectRole, - [Roles.Workspace.Guest]: null - }, - allowed: { - [Roles.Workspace.Admin]: [ - Roles.Stream.Owner, - Roles.Stream.Contributor, - Roles.Stream.Reviewer - ], - [Roles.Workspace.Member]: [ - Roles.Stream.Owner, - Roles.Stream.Contributor, - Roles.Stream.Reviewer - ], - [Roles.Workspace.Guest]: [Roles.Stream.Reviewer, Roles.Stream.Contributor] - } - }), - async *queryAllWorkspaceProjects() { - for (const projIds of chunk(projectIds, 3)) { - yield projIds.map((projId) => ({ id: projId } as unknown as StreamRecord)) - } - }, - getStreamsCollaboratorCounts: async () => { - return {} - }, - setStreamCollaborator: async (params, options) => { - if (!params.role) { - return expect.fail() - } else { - storedRoles.push({ - userId: params.userId, - role: params.role, - projectId: params.streamId - }) - trackProjectUpdate = trackProjectUpdate || options?.trackProjectUpdate - return {} as StreamRecord - } - } - })({ - acl: { - role: Roles.Workspace.Member, - userId, - workspaceId: cryptoRandomString({ length: 10 }) - }, - updatedByUserId: cryptoRandomString({ length: 10 }) - }) - expect(storedRoles).deep.equals( - projectIds.map((projectId) => ({ projectId, role: projectRole, userId })) - ) - expect(trackProjectUpdate).to.not.be.true - }) - }) }) diff --git a/packages/server/modules/workspaces/tests/unit/services/management.spec.ts b/packages/server/modules/workspaces/tests/unit/services/management.spec.ts index 543291bf3..b9d6d3ef2 100644 --- a/packages/server/modules/workspaces/tests/unit/services/management.spec.ts +++ b/packages/server/modules/workspaces/tests/unit/services/management.spec.ts @@ -693,7 +693,11 @@ describe('Workspace role services', () => { workspaceRoles: [role] }) - const deletedRole = await deleteWorkspaceRole({ userId, workspaceId }) + const deletedRole = await deleteWorkspaceRole({ + userId, + workspaceId, + deletedByUserId: cryptoRandomString({ length: 10 }) + }) expect(context.workspaceRoles.length).to.equal(0) expect(deletedRole).to.deep.equal(role) @@ -713,11 +717,19 @@ describe('Workspace role services', () => { workspaceRoles: [role] }) - await deleteWorkspaceRole({ userId, workspaceId }) + const deletedByUserId = cryptoRandomString({ length: 10 }) + await deleteWorkspaceRole({ + userId, + workspaceId, + deletedByUserId + }) expect(context.eventData.isCalled).to.be.true expect(context.eventData.eventName).to.equal(WorkspaceEvents.RoleDeleted) - expect(context.eventData.payload).to.deep.equal({ acl: role }) + expect(context.eventData.payload).to.deep.equal({ + acl: role, + updatedByUserId: deletedByUserId + }) }) it('throws if attempting to delete the last admin from a workspace', async () => { const userId = cryptoRandomString({ length: 10 }) @@ -734,7 +746,13 @@ describe('Workspace role services', () => { workspaceRoles: [role] }) - await expectToThrow(() => deleteWorkspaceRole({ userId, workspaceId })) + await expectToThrow(() => + deleteWorkspaceRole({ + userId, + workspaceId, + deletedByUserId: cryptoRandomString({ length: 10 }) + }) + ) }) it('deletes workspace project roles', async () => { const userId = cryptoRandomString({ length: 10 }) @@ -757,7 +775,11 @@ describe('Workspace role services', () => { ] }) - await deleteWorkspaceRole({ userId, workspaceId }) + await deleteWorkspaceRole({ + userId, + workspaceId, + deletedByUserId: cryptoRandomString({ length: 10 }) + }) expect(context.workspaceProjectRoles.length).to.equal(0) }) @@ -807,13 +829,15 @@ describe('Workspace role services', () => { ...(context.eventData .payload as WorkspaceEventsPayloads[typeof WorkspaceEvents.RoleUpdated]) } - delete payload.flags expect(context.eventData.isCalled).to.be.true expect(context.eventData.eventName).to.equal(WorkspaceEvents.RoleUpdated) expect(payload).to.deep.equal({ acl: role, - updatedByUserId: workspaceOwnerId + updatedByUserId: workspaceOwnerId, + flags: { + skipProjectRoleUpdatesFor: [] + } }) }) it('throws if attempting to remove the last admin in a workspace', async () => { diff --git a/packages/server/modules/workspacesCore/domain/events.ts b/packages/server/modules/workspacesCore/domain/events.ts index 780bd9774..18cbadf49 100644 --- a/packages/server/modules/workspacesCore/domain/events.ts +++ b/packages/server/modules/workspacesCore/domain/events.ts @@ -30,11 +30,12 @@ type WorkspaceCreatedPayload = { type WorkspaceUpdatedPayload = { workspace: Workspace } type WorkspaceRoleDeletedPayload = { acl: Pick + updatedByUserId: string } type WorkspaceRoleUpdatedPayload = { acl: Pick - flags?: { skipProjectRoleUpdatesFor: string[] } updatedByUserId: string + flags?: { skipProjectRoleUpdatesFor: string[] } } type WorkspaceSeatUpdatedPayload = { seat: WorkspaceSeat diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index 0a0d461e0..c58c68f04 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -5490,6 +5490,36 @@ export type GetProjectInvitableCollaboratorsQueryVariables = Exact<{ export type GetProjectInvitableCollaboratorsQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, name: string, invitableCollaborators: { __typename?: 'WorkspaceCollaboratorCollection', totalCount: number, items: Array<{ __typename?: 'WorkspaceCollaborator', id: string, user: { __typename?: 'LimitedUser', name: string } }> } } }; +export type FullPermissionCheckResultFragment = { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record | null }; + +export type ProjectImplicitRoleCheckFragment = { __typename?: 'Project', id: string, role?: string | null, permissions: { __typename?: 'ProjectPermissionChecks', canRead: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record | null }, canReadSettings: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record | null }, canReadWebhooks: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record | null }, canCreateModel: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record | null } } }; + +export type GetUserWorkspaceAccessQueryVariables = Exact<{ + id: Scalars['String']['input']; +}>; + + +export type GetUserWorkspaceAccessQuery = { __typename?: 'Query', workspace: { __typename?: 'Workspace', id: string, role?: string | null, seatType?: WorkspaceSeatType | null } }; + +export type GetUserWorkspaceProjectsWithAccessChecksQueryVariables = Exact<{ + id: Scalars['String']['input']; + limit?: InputMaybe; + cursor?: InputMaybe; + filter?: InputMaybe; +}>; + + +export type GetUserWorkspaceProjectsWithAccessChecksQuery = { __typename?: 'Query', workspace: { __typename?: 'Workspace', role?: string | null, seatType?: WorkspaceSeatType | null, id: string, name: string, slug: string, updatedAt: string, createdAt: string, readOnly: boolean, projects: { __typename?: 'ProjectCollection', cursor?: string | null, totalCount: number, items: Array<{ __typename?: 'Project', id: string, role?: string | null, permissions: { __typename?: 'ProjectPermissionChecks', canRead: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record | null }, canReadSettings: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record | null }, canReadWebhooks: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record | null }, canCreateModel: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record | null } } }> } } }; + +export type GetUserProjectsWithAccessChecksQueryVariables = Exact<{ + limit?: InputMaybe; + cursor?: InputMaybe; + filter?: InputMaybe; +}>; + + +export type GetUserProjectsWithAccessChecksQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', id: string, projects: { __typename?: 'UserProjectCollection', cursor?: string | null, totalCount: number, items: Array<{ __typename?: 'Project', id: string, role?: string | null, permissions: { __typename?: 'ProjectPermissionChecks', canRead: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record | null }, canReadSettings: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record | null }, canReadWebhooks: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record | null }, canCreateModel: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record | null } } }> } } | null }; + export type BasicStreamAccessRequestFieldsFragment = { __typename?: 'StreamAccessRequest', id: string, requesterId: string, streamId: string, createdAt: string, requester: { __typename?: 'LimitedUser', id: string, name: string } }; export type CreateStreamAccessRequestMutationVariables = Exact<{ @@ -6264,6 +6294,8 @@ export type MoveProjectToWorkspaceMutation = { __typename?: 'Mutation', workspac export const BasicWorkspaceFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}}]} as unknown as DocumentNode; export const BasicPendingWorkspaceCollaboratorFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]} as unknown as DocumentNode; export const WorkspaceProjectsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceProjects"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectCollection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]} as unknown as DocumentNode; +export const FullPermissionCheckResultFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullPermissionCheckResult"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PermissionCheckResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authorized"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}}]}}]} as unknown as DocumentNode; +export const ProjectImplicitRoleCheckFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectImplicitRoleCheck"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canRead"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canReadSettings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canReadWebhooks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canCreateModel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullPermissionCheckResult"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PermissionCheckResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authorized"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}}]}}]} as unknown as DocumentNode; export const BasicStreamAccessRequestFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StreamAccessRequest"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"requester"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"requesterId"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode; export const TestAutomateFunctionFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestAutomateFunction"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"repo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"owner"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"releases"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"versionTag"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"inputSchema"}},{"kind":"Field","name":{"kind":"Name","value":"commitId"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"supportedSourceApps"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}}]}}]} as unknown as DocumentNode; export const TestAutomationFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestAutomation"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Automation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"runs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"trigger"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"VersionCreatedTrigger"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"version"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"model"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"functionRuns"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"statusMessage"}},{"kind":"Field","name":{"kind":"Name","value":"contextView"}},{"kind":"Field","name":{"kind":"Name","value":"function"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"elapsed"}},{"kind":"Field","name":{"kind":"Name","value":"results"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"currentRevision"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"triggerDefinitions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"VersionCreatedTriggerDefinition"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"model"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"functions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"parameters"}},{"kind":"Field","name":{"kind":"Name","value":"release"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"function"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"versionTag"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"inputSchema"}},{"kind":"Field","name":{"kind":"Name","value":"commitId"}}]}}]}}]}}]}}]} as unknown as DocumentNode; @@ -6332,6 +6364,9 @@ export const GetWorkspaceWithMembersByRoleDocument = {"kind":"Document","definit export const UpdateWorkspaceProjectRoleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkspaceProjectRole"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectUpdateRoleInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projects"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateRole"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicProjectFields"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicProjectFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"allowPublicComments"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; export const UpdateWorkspaceSeatTypeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkspaceSeatType"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceUpdateSeatTypeInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateSeatType"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"seatType"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const GetProjectInvitableCollaboratorsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProjectInvitableCollaborators"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"search"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"invitableCollaborators"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"search"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetUserWorkspaceAccessDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUserWorkspaceAccess"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"seatType"}}]}}]}}]} as unknown as DocumentNode; +export const GetUserWorkspaceProjectsWithAccessChecksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUserWorkspaceProjectsWithAccessChecks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceProjectsFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"seatType"}},{"kind":"Field","name":{"kind":"Name","value":"projects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectImplicitRoleCheck"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullPermissionCheckResult"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PermissionCheckResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authorized"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectImplicitRoleCheck"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canRead"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canReadSettings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canReadWebhooks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canCreateModel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetUserProjectsWithAccessChecksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUserProjectsWithAccessChecks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UserProjectsFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"projects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectImplicitRoleCheck"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullPermissionCheckResult"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PermissionCheckResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authorized"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectImplicitRoleCheck"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canRead"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canReadSettings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canReadWebhooks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canCreateModel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}}]}}]}}]} as unknown as DocumentNode; export const CreateStreamAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateStreamAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamAccessRequestCreate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StreamAccessRequest"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"requester"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"requesterId"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode; export const GetStreamAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetStreamAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamAccessRequest"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StreamAccessRequest"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"requester"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"requesterId"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode; export const GetFullStreamAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetFullStreamAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamAccessRequest"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"}},{"kind":"Field","name":{"kind":"Name","value":"stream"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StreamAccessRequest"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"requester"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"requesterId"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode; diff --git a/utils/helm/speckle-server/values.schema.json b/utils/helm/speckle-server/values.schema.json index 0891df75c..089e75eeb 100644 --- a/utils/helm/speckle-server/values.schema.json +++ b/utils/helm/speckle-server/values.schema.json @@ -244,7 +244,7 @@ "maxConnectionsServer": { "type": "number", "description": "The number of connections to the Postgres database to provide in the connection pool", - "default": 4 + "default": 8 }, "certificate": { "type": "string", diff --git a/utils/helm/speckle-server/values.yaml b/utils/helm/speckle-server/values.yaml index ee18d279d..3a7c9a243 100644 --- a/utils/helm/speckle-server/values.yaml +++ b/utils/helm/speckle-server/values.yaml @@ -191,7 +191,7 @@ db: useCertificate: false ## @param db.maxConnectionsServer The number of connections to the Postgres database to provide in the connection pool ## - maxConnectionsServer: 4 + maxConnectionsServer: 8 ## @param db.certificate The x509 public certificate for SSL connections to the Postgres database. Use of this certificate requires db.useCertificate to be enabled and an appropriate value for db.PGSSLMODE provided. ## The value must be formatted as a multi-line string. We recommend using the pipe-symbol and taking care to ## indent all lines of the value correctly.