Merge branch 'main' into iain/web-2732-observability-for-improved-reliability-workspaces
This commit is contained in:
@@ -119,7 +119,7 @@ import {
|
||||
} from '@/modules/core/services/streams/access'
|
||||
import { getUserFactory } from '@/modules/core/repositories/users'
|
||||
import { authorizeResolver } from '@/modules/shared'
|
||||
import { isNewPaidPlanType, isNewPlanType } from '@/modules/gatekeeper/helpers/plans'
|
||||
import { isNewPlanType } from '@/modules/gatekeeper/helpers/plans'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
|
||||
const { FF_BILLING_INTEGRATION_ENABLED } = getFeatureFlags()
|
||||
@@ -292,7 +292,7 @@ export const onWorkspaceSeatUpdatedFactory =
|
||||
if (!workspace || !role) return
|
||||
|
||||
// Only new plans only rely on seat types
|
||||
const isNewPlan = workspace.plan && isNewPaidPlanType(workspace.plan.name)
|
||||
const isNewPlan = workspace.plan && isNewPlanType(workspace.plan.name)
|
||||
if (!isNewPlan) {
|
||||
return
|
||||
}
|
||||
@@ -353,7 +353,7 @@ export const onWorkspaceRoleUpdatedFactory =
|
||||
if (!workspace) return
|
||||
|
||||
// New plans don't do automatic project role assignment
|
||||
const isNewPlan = workspace.plan && isNewPaidPlanType(workspace.plan.name)
|
||||
const isNewPlan = workspace.plan && isNewPlanType(workspace.plan.name)
|
||||
if (isNewPlan) {
|
||||
return
|
||||
}
|
||||
@@ -630,10 +630,19 @@ const blockInvalidWorkspaceProjectRoleUpdatesFactory =
|
||||
let allowedRoles: StreamRoles[]
|
||||
const isNewPlan = workspace.plan && isNewPlanType(workspace.plan.name)
|
||||
if (isNewPlan) {
|
||||
const seatRoleMapping = await deps.getWorkspaceSeatTypeToProjectRoleMapping({
|
||||
workspaceId: project.workspaceId
|
||||
})
|
||||
allowedRoles = seatRoleMapping.allowed[seatType]
|
||||
const workspaceAllowedRoles = (
|
||||
await deps.getWorkspaceRoleToDefaultProjectRoleMapping({
|
||||
workspaceId: project.workspaceId
|
||||
})
|
||||
).allowed[workspaceRole]
|
||||
const seatAllowedRoles = (
|
||||
await deps.getWorkspaceSeatTypeToProjectRoleMapping({
|
||||
workspaceId: project.workspaceId
|
||||
})
|
||||
).allowed[seatType]
|
||||
allowedRoles = Array.from(
|
||||
new Set(workspaceAllowedRoles).intersection(new Set(seatAllowedRoles))
|
||||
)
|
||||
} else {
|
||||
const roleMapping = await deps.getWorkspaceRoleToDefaultProjectRoleMapping({
|
||||
workspaceId: project.workspaceId
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { db } from '@/db/knex'
|
||||
import { Resolvers } from '@/modules/core/graph/generated/graphql'
|
||||
import {
|
||||
Resolvers,
|
||||
TokenResourceIdentifierType
|
||||
} from '@/modules/core/graph/generated/graphql'
|
||||
import { removePrivateFields } from '@/modules/core/helpers/userHelper'
|
||||
import {
|
||||
updateProjectFactory,
|
||||
@@ -205,6 +208,7 @@ import {
|
||||
getWorkspaceRolesAndSeatsFactory,
|
||||
getWorkspaceUserSeatFactory
|
||||
} from '@/modules/gatekeeper/repositories/workspaceSeat'
|
||||
import { throwIfResourceAccessNotAllowed } from '@/modules/core/helpers/token'
|
||||
import {
|
||||
mapAuthToServerError,
|
||||
throwIfAuthNotOk
|
||||
@@ -317,6 +321,7 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
if (!workspace) {
|
||||
throw new WorkspaceNotFoundError()
|
||||
}
|
||||
|
||||
await authorizeResolver(
|
||||
ctx.userId,
|
||||
workspace.id,
|
||||
@@ -369,13 +374,7 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
},
|
||||
ProjectInviteMutations: {
|
||||
async createForWorkspace(_parent, args, ctx) {
|
||||
const projectId = args.projectId
|
||||
await authorizeResolver(
|
||||
ctx.userId,
|
||||
projectId,
|
||||
Roles.Stream.Owner,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
const { projectId } = args
|
||||
|
||||
const inviteCount = args.inputs.length
|
||||
if (inviteCount > 10 && ctx.role !== Roles.Server.Admin) {
|
||||
@@ -384,12 +383,26 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
)
|
||||
}
|
||||
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: projectId,
|
||||
resourceType: TokenResourceIdentifierType.Project,
|
||||
resourceAccessRules: ctx.resourceAccessRules
|
||||
})
|
||||
|
||||
const logger = ctx.log.child({
|
||||
projectId,
|
||||
streamId: projectId, //legacy
|
||||
inviteCount
|
||||
})
|
||||
|
||||
const canInvite = await ctx.authPolicies.project.canInvite({
|
||||
userId: ctx.userId,
|
||||
projectId
|
||||
})
|
||||
if (!canInvite.isOk) {
|
||||
throw mapAuthToServerError(canInvite.error)
|
||||
}
|
||||
|
||||
const createProjectInvite = createProjectInviteFactory({
|
||||
createAndSendInvite: buildCreateAndSendServerOrProjectInvite(),
|
||||
getStream
|
||||
@@ -1037,12 +1050,19 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
input: { inviteId, workspaceId }
|
||||
} = args
|
||||
|
||||
await authorizeResolver(
|
||||
ctx.userId!,
|
||||
workspaceId,
|
||||
Roles.Workspace.Admin,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: workspaceId,
|
||||
resourceType: TokenResourceIdentifierType.Workspace,
|
||||
resourceAccessRules: ctx.resourceAccessRules
|
||||
})
|
||||
|
||||
const canInvite = await ctx.authPolicies.workspace.canInvite({
|
||||
userId: ctx.userId,
|
||||
workspaceId
|
||||
})
|
||||
if (!canInvite.isOk) {
|
||||
throw mapAuthToServerError(canInvite.error)
|
||||
}
|
||||
|
||||
const logger = ctx.log.child({
|
||||
workspaceId,
|
||||
@@ -1083,11 +1103,26 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
return true
|
||||
},
|
||||
create: async (_parent, args, ctx) => {
|
||||
const workspaceId = args.workspaceId
|
||||
const { workspaceId } = args
|
||||
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: workspaceId,
|
||||
resourceType: TokenResourceIdentifierType.Workspace,
|
||||
resourceAccessRules: ctx.resourceAccessRules
|
||||
})
|
||||
|
||||
const logger = ctx.log.child({
|
||||
workspaceId
|
||||
})
|
||||
|
||||
const canInvite = await ctx.authPolicies.workspace.canInvite({
|
||||
userId: ctx.userId,
|
||||
workspaceId
|
||||
})
|
||||
if (!canInvite.isOk) {
|
||||
throw mapAuthToServerError(canInvite.error)
|
||||
}
|
||||
|
||||
const createInvite = createWorkspaceInviteFactory({
|
||||
createAndSendInvite: buildCreateAndSendWorkspaceInvite()
|
||||
})
|
||||
@@ -1109,7 +1144,8 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
return ctx.loaders.workspaces!.getWorkspace.load(workspaceId)
|
||||
},
|
||||
batchCreate: async (_parent, args, ctx) => {
|
||||
const workspaceId = args.workspaceId
|
||||
const { workspaceId } = args
|
||||
|
||||
const inviteCount = args.input.length
|
||||
if (inviteCount > 10 && ctx.role !== Roles.Server.Admin) {
|
||||
throw new InviteCreateValidationError(
|
||||
@@ -1117,11 +1153,25 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
)
|
||||
}
|
||||
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: workspaceId,
|
||||
resourceType: TokenResourceIdentifierType.Workspace,
|
||||
resourceAccessRules: ctx.resourceAccessRules
|
||||
})
|
||||
|
||||
const logger = ctx.log.child({
|
||||
workspaceId,
|
||||
inviteCount
|
||||
})
|
||||
|
||||
const canInvite = await ctx.authPolicies.workspace.canInvite({
|
||||
userId: ctx.userId,
|
||||
workspaceId
|
||||
})
|
||||
if (!canInvite.isOk) {
|
||||
throw mapAuthToServerError(canInvite.error)
|
||||
}
|
||||
|
||||
const createInvite = createWorkspaceInviteFactory({
|
||||
createAndSendInvite: buildCreateAndSendWorkspaceInvite()
|
||||
})
|
||||
@@ -1222,14 +1272,21 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
return true
|
||||
},
|
||||
cancel: async (_parent, args, ctx) => {
|
||||
const workspaceId = args.workspaceId
|
||||
const inviteId = args.inviteId
|
||||
await authorizeResolver(
|
||||
ctx.userId,
|
||||
workspaceId,
|
||||
Roles.Workspace.Admin,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
const { workspaceId, inviteId } = args
|
||||
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: workspaceId,
|
||||
resourceType: TokenResourceIdentifierType.Workspace,
|
||||
resourceAccessRules: ctx.resourceAccessRules
|
||||
})
|
||||
|
||||
const canInvite = await ctx.authPolicies.workspace.canInvite({
|
||||
userId: ctx.userId,
|
||||
workspaceId
|
||||
})
|
||||
if (!canInvite.isOk) {
|
||||
throw mapAuthToServerError(canInvite.error)
|
||||
}
|
||||
|
||||
const logger = ctx.log.child({
|
||||
workspaceId,
|
||||
@@ -1881,12 +1938,19 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
if (!requestedWorkspaceId) return false
|
||||
|
||||
if (payload.workspaceId !== requestedWorkspaceId) return false
|
||||
await authorizeResolver(
|
||||
ctx.userId!,
|
||||
payload.workspaceId,
|
||||
Roles.Workspace.Guest,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
|
||||
// TODO: Subs dont clear until actual response!! formatResponse/formatError, doesn't kick in
|
||||
// if this handler returns false
|
||||
const projectId = payload.workspaceProjectsUpdated.projectId
|
||||
const canGetMessage =
|
||||
await ctx.authPolicies.workspace.canReceiveProjectsUpdatedMessage({
|
||||
userId: ctx.userId,
|
||||
projectId,
|
||||
workspaceId: requestedWorkspaceId
|
||||
})
|
||||
if (canGetMessage.isErr) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ import {
|
||||
GetWorkspaceWithPlan,
|
||||
WorkspaceSeatType
|
||||
} from '@/modules/gatekeeper/domain/billing'
|
||||
import { isNewPaidPlanType } from '@/modules/gatekeeper/helpers/plans'
|
||||
import { isNewPlanType } from '@/modules/gatekeeper/helpers/plans'
|
||||
import { NotImplementedError } from '@/modules/shared/errors'
|
||||
import { FindEmailsByUserId } from '@/modules/core/domain/userEmails/operations'
|
||||
import { userEmailsCompliantWithWorkspaceDomains } from '@/modules/workspaces/domain/logic'
|
||||
@@ -226,32 +226,38 @@ export const getWorkspaceRoleToDefaultProjectRoleMappingFactory =
|
||||
throw new WorkspaceNotFoundError()
|
||||
}
|
||||
|
||||
const isNewPlan = workspace.plan && isNewPaidPlanType(workspace.plan.name)
|
||||
if (isNewPlan) {
|
||||
throw new NotImplementedError(
|
||||
'This function is not supported for this workspace plan'
|
||||
)
|
||||
const isNewPlan = workspace.plan && isNewPlanType(workspace.plan.name)
|
||||
const allowed = {
|
||||
[Roles.Workspace.Guest]: [Roles.Stream.Reviewer, Roles.Stream.Contributor],
|
||||
[Roles.Workspace.Member]: [
|
||||
Roles.Stream.Reviewer,
|
||||
Roles.Stream.Contributor,
|
||||
Roles.Stream.Owner
|
||||
],
|
||||
[Roles.Workspace.Admin]: [
|
||||
Roles.Stream.Reviewer,
|
||||
Roles.Stream.Contributor,
|
||||
Roles.Stream.Owner
|
||||
]
|
||||
}
|
||||
|
||||
if (isNewPlan)
|
||||
return {
|
||||
default: {
|
||||
[Roles.Workspace.Guest]: null,
|
||||
[Roles.Workspace.Member]: null,
|
||||
[Roles.Workspace.Admin]: null
|
||||
},
|
||||
allowed
|
||||
}
|
||||
|
||||
return {
|
||||
default: {
|
||||
[Roles.Workspace.Guest]: null,
|
||||
[Roles.Workspace.Member]: Roles.Stream.Reviewer,
|
||||
[Roles.Workspace.Admin]: Roles.Stream.Owner
|
||||
},
|
||||
allowed: {
|
||||
[Roles.Workspace.Guest]: [Roles.Stream.Reviewer, Roles.Stream.Contributor],
|
||||
[Roles.Workspace.Member]: [
|
||||
Roles.Stream.Reviewer,
|
||||
Roles.Stream.Contributor,
|
||||
Roles.Stream.Owner
|
||||
],
|
||||
[Roles.Workspace.Admin]: [
|
||||
Roles.Stream.Reviewer,
|
||||
Roles.Stream.Contributor,
|
||||
Roles.Stream.Owner
|
||||
]
|
||||
}
|
||||
allowed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,7 +273,7 @@ export const getWorkspaceSeatTypeToProjectRoleMappingFactory =
|
||||
throw new WorkspaceNotFoundError()
|
||||
}
|
||||
|
||||
const isNewPlan = workspace.plan && isNewPaidPlanType(workspace.plan.name)
|
||||
const isNewPlan = workspace.plan && isNewPlanType(workspace.plan.name)
|
||||
if (!isNewPlan) {
|
||||
throw new NotImplementedError(
|
||||
'This function is not supported for this workspace plan'
|
||||
|
||||
@@ -186,9 +186,7 @@ describe('Workspaces Invites GQL', () => {
|
||||
}
|
||||
})
|
||||
|
||||
expect(res).to.haveGraphQLErrors(
|
||||
'Attempting to invite into a non-existant workspace'
|
||||
)
|
||||
expect(res).to.haveGraphQLErrors('You do not have access to the workspace')
|
||||
expect(res.data?.workspaceMutations?.invites?.create).to.not.be.ok
|
||||
})
|
||||
|
||||
@@ -226,7 +224,9 @@ describe('Workspaces Invites GQL', () => {
|
||||
}
|
||||
})
|
||||
|
||||
expect(res).to.haveGraphQLErrors('You are not authorized')
|
||||
expect(res).to.haveGraphQLErrors(
|
||||
'You do not have enough permissions in the workspace to perform this action'
|
||||
)
|
||||
expect(res.data?.workspaceMutations?.invites?.create).to.not.be.ok
|
||||
})
|
||||
|
||||
@@ -269,7 +269,9 @@ describe('Workspaces Invites GQL', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
expect(res).to.haveGraphQLErrors('You are not authorized')
|
||||
expect(res).to.haveGraphQLErrors(
|
||||
'You do not have enough permissions in the workspace to perform this action'
|
||||
)
|
||||
expect(res.data?.workspaceMutations?.invites?.batchCreate).to.not.be.ok
|
||||
})
|
||||
|
||||
@@ -739,7 +741,9 @@ describe('Workspaces Invites GQL', () => {
|
||||
}
|
||||
})
|
||||
|
||||
expect(res).to.haveGraphQLErrors('You are not authorized')
|
||||
expect(res).to.haveGraphQLErrors(
|
||||
'You do not have enough permissions in the workspace to perform this action'
|
||||
)
|
||||
expect(res.data?.workspaceMutations?.invites?.cancel).to.not.be.ok
|
||||
|
||||
const invite = await findInviteFactory({ db })({
|
||||
|
||||
@@ -49,10 +49,7 @@ import {
|
||||
createRandomEmail,
|
||||
createRandomString
|
||||
} from '@/modules/core/helpers/testHelpers'
|
||||
import {
|
||||
getWorkspaceFactory,
|
||||
getWorkspaceRoleForUserFactory
|
||||
} from '@/modules/workspaces/repositories/workspaces'
|
||||
import { getWorkspaceRoleForUserFactory } from '@/modules/workspaces/repositories/workspaces'
|
||||
import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams'
|
||||
import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace'
|
||||
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
|
||||
@@ -489,14 +486,20 @@ describe('Workspaces GQL CRUD', () => {
|
||||
})
|
||||
|
||||
it('should return workspace team projectRoles', async () => {
|
||||
const createRes = await apollo.execute(CreateWorkspaceDocument, {
|
||||
input: { name: createRandomString() }
|
||||
// create workspace w/ infinite limits (otherwise test fails)
|
||||
const workspace: BasicTestWorkspace = {
|
||||
name: createRandomString(),
|
||||
id: '',
|
||||
ownerId: '',
|
||||
slug: ''
|
||||
}
|
||||
await createTestWorkspace(workspace, testMemberUser, {
|
||||
addPlan: {
|
||||
name: 'teamUnlimited',
|
||||
status: 'valid'
|
||||
}
|
||||
})
|
||||
expect(createRes).to.not.haveGraphQLErrors()
|
||||
const workspaceId = createRes.data!.workspaceMutations.create.id
|
||||
const workspace = (await getWorkspaceFactory({ db })({
|
||||
workspaceId
|
||||
})) as unknown as BasicTestWorkspace
|
||||
const workspaceId = workspace.id
|
||||
|
||||
const member = {
|
||||
id: createRandomString(),
|
||||
@@ -1082,7 +1085,7 @@ describe('Workspaces GQL CRUD', () => {
|
||||
})
|
||||
|
||||
expect(deleteRes).to.not.haveGraphQLErrors()
|
||||
expect(getRes).to.haveGraphQLErrors('Workspace not found')
|
||||
expect(getRes).to.haveGraphQLErrors({ code: WorkspaceNotFoundError.code })
|
||||
})
|
||||
|
||||
it('should throw if non-workspace-admin triggers delete', async () => {
|
||||
|
||||
Reference in New Issue
Block a user