Merge pull request #3699 from specklesystems/alessandro/web-2309-return-error-on-version-creation-for-projects-in-readonly

Alessandro/web 2309 return error on version creation for projects in readonly
This commit is contained in:
Alessandro Magionami
2024-12-23 10:43:22 +01:00
committed by GitHub
7 changed files with 292 additions and 13 deletions
@@ -82,6 +82,7 @@ import {
getRegisteredDbClients
} from '@/modules/multiregion/utils/dbSelector'
import { LegacyUserCommit } from '@/modules/core/domain/commits/types'
import coreModule from '@/modules/core'
const getStreams = getStreamsFactory({ db })
@@ -323,6 +324,10 @@ export = {
},
Mutation: {
async commitCreate(_parent, args, context) {
await coreModule.executeHooks('onCreateVersionRequest', {
projectId: args.commit.streamId
})
const projectDb = await getProjectDbClient({ projectId: args.commit.streamId })
await authorizeResolver(
context.userId,
@@ -57,6 +57,7 @@ import {
import { getObjectFactory } from '@/modules/core/repositories/objects'
import { saveActivityFactory } from '@/modules/activitystream/repositories'
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
import coreModule from '@/modules/core'
export = {
Project: {
@@ -177,6 +178,10 @@ export = {
ctx.resourceAccessRules
)
await coreModule.executeHooks('onCreateVersionRequest', {
projectId: args.input.projectId
})
const rateLimitResult = await getRateLimitResult('COMMIT_CREATE', ctx.userId!)
if (isRateLimitBreached(rateLimitResult)) {
throw new RateLimitError(rateLimitResult)
+8 -1
View File
@@ -4,11 +4,18 @@ type OnCreateObjectRequest = ({
projectId: string
}) => Promise<void> | void
type OnCreateVersionRequest = ({
projectId
}: {
projectId: string
}) => Promise<void> | void
export type HooksConfig = {
onCreateObjectRequest: OnCreateObjectRequest[]
onCreateVersionRequest: OnCreateVersionRequest[]
}
export type Hook = OnCreateObjectRequest
export type Hook = OnCreateObjectRequest | OnCreateVersionRequest
export type ExecuteHooks = (
key: keyof HooksConfig,
+2 -1
View File
@@ -28,7 +28,8 @@ const coreModule: SpeckleModule<{
executeHooks: ExecuteHooks
}> = {
hooks: {
onCreateObjectRequest: []
onCreateObjectRequest: [],
onCreateVersionRequest: []
},
addHook(key: keyof HooksConfig, callback: Hook) {
this.hooks[key].push(callback)
@@ -0,0 +1,132 @@
import { beforeEachContext } from '@/test/hooks'
import { expect } from 'chai'
import { describe, it } from 'mocha'
import {
createRandomEmail,
createRandomPassword
} from '@/modules/core/helpers/testHelpers'
import {
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory,
findEmailFactory
} from '@/modules/core/repositories/userEmails'
import { db } from '@/db/knex'
import { testApolloServer } from '@/test/graphqlHelper'
import {
CreateWorkspaceDocument,
CreateWorkspaceProjectDocument
} from '@/test/graphql/generated/graphql'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import {
countAdminUsersFactory,
legacyGetUserFactory,
storeUserAclFactory,
storeUserFactory
} from '@/modules/core/repositories/users'
import { createUserFactory } from '@/modules/core/services/users/management'
import { UsersEmitter } from '@/modules/core/events/usersEmitter'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { WorkspaceReadOnlyError } from '@/modules/gatekeeper/errors/billing'
import gql from 'graphql-tag'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
const getServerInfo = getServerInfoFactory({ db })
const getUser = legacyGetUserFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail: findEmailFactory({ db }),
getUser,
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUserEmail = validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail: findEmailFactory({ db }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
})
const findEmail = findEmailFactory({ db })
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: createUserEmail,
usersEventsEmitter: UsersEmitter.emit
})
const { FF_BILLING_INTEGRATION_ENABLED } = getFeatureFlags()
const createCommitMutation = gql`
mutation CreateCommit($commit: CommitCreateInput!) {
commitCreate(commit: $commit)
}
`
describe('Commits graphql @core', () => {
before(async () => {
await beforeEachContext()
})
describe('Create commit mutation', () => {
;(FF_BILLING_INTEGRATION_ENABLED ? it : it.skip)(
'should return error if project is read-only',
async () => {
const userId = await createUser({
name: 'emails user',
email: createRandomEmail(),
password: createRandomPassword()
})
const apollo = await testApolloServer({ authUserId: userId })
const workspaceCreateRes = await apollo.execute(CreateWorkspaceDocument, {
input: { name: 'test ws' }
})
expect(workspaceCreateRes).to.not.haveGraphQLErrors()
const workspace = workspaceCreateRes.data?.workspaceMutations.create
const projectCreateRes = await apollo.execute(CreateWorkspaceProjectDocument, {
input: { workspaceId: workspace!.id, name: 'test project' }
})
expect(projectCreateRes).to.not.haveGraphQLErrors()
const project = projectCreateRes.data?.workspaceMutations.projects.create
// Make the project read-only
await db('workspace_plans')
.update({ status: 'canceled' })
.where({ workspaceId: workspace!.id })
const versionCreateRes = await apollo.execute(createCommitMutation, {
commit: {
streamId: project!.id,
branchName: 'branch',
objectId: 'objectid'
}
})
expect(versionCreateRes).to.haveGraphQLErrors()
expect(versionCreateRes.errors).to.have.length(1)
expect(versionCreateRes.errors![0].message).to.eq(
new WorkspaceReadOnlyError().message
)
}
)
})
})
@@ -0,0 +1,128 @@
import { beforeEachContext } from '@/test/hooks'
import { expect } from 'chai'
import { describe, it } from 'mocha'
import {
createRandomEmail,
createRandomPassword
} from '@/modules/core/helpers/testHelpers'
import {
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory,
findEmailFactory
} from '@/modules/core/repositories/userEmails'
import { db } from '@/db/knex'
import { testApolloServer } from '@/test/graphqlHelper'
import {
CreateProjectVersionDocument,
CreateWorkspaceDocument,
CreateWorkspaceProjectDocument
} from '@/test/graphql/generated/graphql'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import {
countAdminUsersFactory,
legacyGetUserFactory,
storeUserAclFactory,
storeUserFactory
} from '@/modules/core/repositories/users'
import { createUserFactory } from '@/modules/core/services/users/management'
import { UsersEmitter } from '@/modules/core/events/usersEmitter'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { WorkspaceReadOnlyError } from '@/modules/gatekeeper/errors/billing'
import { CreateVersionInput } from '@/modules/core/graph/generated/graphql'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
const getServerInfo = getServerInfoFactory({ db })
const getUser = legacyGetUserFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail: findEmailFactory({ db }),
getUser,
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUserEmail = validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail: findEmailFactory({ db }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
})
const findEmail = findEmailFactory({ db })
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: createUserEmail,
usersEventsEmitter: UsersEmitter.emit
})
const { FF_BILLING_INTEGRATION_ENABLED } = getFeatureFlags()
describe('Versions graphql @core', () => {
before(async () => {
await beforeEachContext()
})
describe('Create version mutation', () => {
;(FF_BILLING_INTEGRATION_ENABLED ? it : it.skip)(
'should return error if project is read-only',
async () => {
const userId = await createUser({
name: 'emails user',
email: createRandomEmail(),
password: createRandomPassword()
})
const apollo = await testApolloServer({ authUserId: userId })
const workspaceCreateRes = await apollo.execute(CreateWorkspaceDocument, {
input: { name: 'test ws' }
})
expect(workspaceCreateRes).to.not.haveGraphQLErrors()
const workspace = workspaceCreateRes.data?.workspaceMutations.create
const projectCreateRes = await apollo.execute(CreateWorkspaceProjectDocument, {
input: { workspaceId: workspace!.id, name: 'test project' }
})
expect(projectCreateRes).to.not.haveGraphQLErrors()
const project = projectCreateRes.data?.workspaceMutations.projects.create
// Make the project read-only
await db('workspace_plans')
.update({ status: 'canceled' })
.where({ workspaceId: workspace!.id })
const versionCreateRes = await apollo.execute(CreateProjectVersionDocument, {
input: {
projectId: project!.id,
modelId: 'modelid',
objectId: 'objectid'
} as unknown as CreateVersionInput
})
expect(versionCreateRes).to.haveGraphQLErrors()
expect(versionCreateRes.errors).to.have.length(1)
expect(versionCreateRes.errors![0].message).to.eq(
new WorkspaceReadOnlyError().message
)
}
)
})
})
+12 -11
View File
@@ -172,17 +172,18 @@ const gatekeeperModule: SpeckleModule = {
})
},
async finalize() {
coreModule.addHook(
'onCreateObjectRequest',
async function isProjectReadOnly({ projectId }) {
const readOnly = await isProjectReadOnlyFactory({
getWorkspacePlanByProjectId: getWorkspacePlanByProjectIdFactory({
db
})
})({ projectId })
if (readOnly) throw new WorkspaceReadOnlyError()
}
)
coreModule.addHook('onCreateObjectRequest', isProjectReadOnly)
coreModule.addHook('onCreateVersionRequest', isProjectReadOnly)
}
}
async function isProjectReadOnly({ projectId }: { projectId: string }) {
const readOnly = await isProjectReadOnlyFactory({
getWorkspacePlanByProjectId: getWorkspacePlanByProjectIdFactory({
db
})
})({ projectId })
if (readOnly) throw new WorkspaceReadOnlyError()
}
export = gatekeeperModule