diff --git a/packages/frontend-2/nuxt.config.ts b/packages/frontend-2/nuxt.config.ts index f4fb401c1..7e4655510 100644 --- a/packages/frontend-2/nuxt.config.ts +++ b/packages/frontend-2/nuxt.config.ts @@ -24,6 +24,7 @@ const buildSourceMaps = ['1', 'true', true, 1].includes(BUILD_SOURCEMAPS) // https://v3.nuxtjs.org/api/configuration/nuxt.config export default defineNuxtConfig({ + // ssr: false, // for debugging set to false (prod should always be true) ...(buildSourceMaps ? { sourcemap: true } : {}), modulesDir: ['./node_modules'], typescript: { @@ -146,21 +147,6 @@ export default defineNuxtConfig({ }, routeRules: { - // Necessary because of redirects from backend in auth flows - '/': { - cors: true, - headers: { - 'access-control-allow-methods': 'GET', - 'Access-Control-Expose-Headers': '*' - } - }, - '/authn/login': { - cors: true, - headers: { - 'access-control-allow-methods': 'GET', - 'Access-Control-Expose-Headers': '*' - } - }, '/functions': { redirect: { to: '/', diff --git a/packages/frontend-2/server/middleware/003-cors.ts b/packages/frontend-2/server/middleware/003-cors.ts new file mode 100644 index 000000000..2b4b213fd --- /dev/null +++ b/packages/frontend-2/server/middleware/003-cors.ts @@ -0,0 +1,32 @@ +import { RelativeURL } from '@speckle/shared' + +const corsRoutes = ['/', '/authn/login'] + +/** + * CORS settings in nuxt config routeRules suck - with those, OPTIONS requests can still trigger redirects as if the + * req is a normal GET request, which is not supported. So we're implementing CORS ourselves here. + */ +export default defineEventHandler((event) => { + const optionsResponse = (code: number) => new Response(null, { status: code }) + + // Get path w/o querystring + const path = new RelativeURL(event.path).pathname + + // For CORS routes - allow all origins on GET (necessary for authentication fetch calls) + if (corsRoutes.includes(path)) { + setHeaders(event, { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET', + 'Access-Control-Expose-Headers': '*', + 'Access-Control-Allow-Headers': '*', + 'Access-Control-Max-Age': '0' + }) + + if (event.method === 'OPTIONS') return optionsResponse(204) + } + + // For other routes CORS is disabled + if (event.method === 'OPTIONS') { + return optionsResponse(403) + } +}) diff --git a/packages/shared/src/authz/fragments/workspaces.ts b/packages/shared/src/authz/fragments/workspaces.ts index 0efa30fd0..f0ad11620 100644 --- a/packages/shared/src/authz/fragments/workspaces.ts +++ b/packages/shared/src/authz/fragments/workspaces.ts @@ -5,6 +5,7 @@ import { hasMinimumWorkspaceRole } from '../checks/workspaceRole.js' import { + PersonalProjectsLimitedError, ProjectNotFoundError, WorkspaceLimitsReachedError, WorkspaceNoAccessError, @@ -214,6 +215,7 @@ export const ensureWorkspaceProjectCanBeCreatedFragment: AuthPolicyEnsureFragmen * If userId is specified, will also check for appropriate user role & seat */ export const ensureModelCanBeCreatedFragment: AuthPolicyEnsureFragment< + | typeof Loaders.getEnv | typeof Loaders.getWorkspacePlan | typeof Loaders.getWorkspaceRole | typeof Loaders.getWorkspaceLimits @@ -232,55 +234,72 @@ export const ensureModelCanBeCreatedFragment: AuthPolicyEnsureFragment< | typeof WorkspaceReadOnlyError | typeof WorkspaceLimitsReachedError | typeof ProjectNotFoundError + | typeof PersonalProjectsLimitedError > > = (loaders) => async ({ projectId, userId, addedModelCount, workspaceId }) => { addedModelCount = addedModelCount ?? 1 + + const { FF_WORKSPACES_MODULE_ENABLED, FF_PERSONAL_PROJECTS_LIMITS_ENABLED } = + await loaders.getEnv() const project = await loaders.getProject({ projectId }) if (!project) return err(new ProjectNotFoundError()) // Project may not be attached to a workspace yet, then we use the specified workspaceId workspaceId = workspaceId || project.workspaceId || undefined - if (!workspaceId) return ok() - if (userId) { - // Has workspace role - const isInWorkspace = await hasAnyWorkspaceRole(loaders)({ - userId, + // If workspace + if (workspaceId && FF_WORKSPACES_MODULE_ENABLED) { + if (userId) { + // Has workspace role + const isInWorkspace = await hasAnyWorkspaceRole(loaders)({ + userId, + workspaceId + }) + if (!isInWorkspace) { + return err(new WorkspaceNoAccessError()) + } + } + + const ensuredNotReadOnly = await ensureWorkspaceNotReadOnlyFragment(loaders)({ workspaceId }) - if (!isInWorkspace) { - return err(new WorkspaceNoAccessError()) - } - } + if (ensuredNotReadOnly.isErr) return err(ensuredNotReadOnly.error) - const ensuredNotReadOnly = await ensureWorkspaceNotReadOnlyFragment(loaders)({ - workspaceId - }) - if (ensuredNotReadOnly.isErr) return err(ensuredNotReadOnly.error) + const workspacePlan = await loaders.getWorkspacePlan({ workspaceId }) + if (!workspacePlan) return err(new WorkspaceNoAccessError()) - const workspacePlan = await loaders.getWorkspacePlan({ workspaceId }) - if (!workspacePlan) return err(new WorkspaceNoAccessError()) + const workspaceLimits = await loaders.getWorkspaceLimits({ workspaceId }) + if (!workspaceLimits) return err(new WorkspaceNoAccessError()) - const workspaceLimits = await loaders.getWorkspaceLimits({ workspaceId }) - if (!workspaceLimits) return err(new WorkspaceNoAccessError()) + if (workspaceLimits.modelCount === null) return ok() - if (workspaceLimits.modelCount === null) return ok() + const currentModelCount = await loaders.getWorkspaceModelCount({ workspaceId }) - const currentModelCount = await loaders.getWorkspaceModelCount({ workspaceId }) + if (currentModelCount === null) return err(new WorkspaceNoAccessError()) - if (currentModelCount === null) return err(new WorkspaceNoAccessError()) - - return currentModelCount + addedModelCount <= workspaceLimits.modelCount - ? ok() - : err( - new WorkspaceLimitsReachedError({ - message: - 'You have reached the maximum number of models for your plan. Upgrade to increase it.', - payload: { - limit: 'modelCount' - } - }) + return currentModelCount + addedModelCount <= workspaceLimits.modelCount + ? ok() + : err( + new WorkspaceLimitsReachedError({ + message: + 'You have reached the maximum number of models for your plan. Upgrade to increase it.', + payload: { + limit: 'modelCount' + } + }) + ) + } else { + // If not - check personal project limits + if (FF_PERSONAL_PROJECTS_LIMITS_ENABLED) { + return err( + new PersonalProjectsLimitedError( + 'No new models can be added to personal projects' + ) ) + } + + return ok() + } } diff --git a/packages/shared/src/authz/policies/project/canMoveToWorkspace.ts b/packages/shared/src/authz/policies/project/canMoveToWorkspace.ts index ac5a0a910..c6ae4671c 100644 --- a/packages/shared/src/authz/policies/project/canMoveToWorkspace.ts +++ b/packages/shared/src/authz/policies/project/canMoveToWorkspace.ts @@ -1,5 +1,6 @@ import { err, ok } from 'true-myth/result' import { + PersonalProjectsLimitedError, ProjectNoAccessError, ProjectNotEnoughPermissionsError, ProjectNotFoundError, @@ -65,6 +66,7 @@ type PolicyErrors = | InstanceType | InstanceType | InstanceType + | InstanceType export const canMoveToWorkspacePolicy: AuthPolicy< PolicyLoaderKeys, diff --git a/packages/shared/src/authz/policies/project/model/canCreate.spec.ts b/packages/shared/src/authz/policies/project/model/canCreate.spec.ts index 9a7a2272d..1efefc2ce 100644 --- a/packages/shared/src/authz/policies/project/model/canCreate.spec.ts +++ b/packages/shared/src/authz/policies/project/model/canCreate.spec.ts @@ -21,7 +21,11 @@ const buildCanCreateModelPolicy = ( overrides?: Partial[0]> ) => canCreateModelPolicy({ - getEnv: async () => parseFeatureFlags({}), + getEnv: async () => + parseFeatureFlags({ + FF_WORKSPACES_MODULE_ENABLED: 'true', + FF_PERSONAL_PROJECTS_LIMITS_ENABLED: 'false' + }), getProject: getProjectFake({ id: cryptoRandomString({ length: 9 }), workspaceId: cryptoRandomString({ length: 9 }) @@ -184,4 +188,22 @@ describe('canCreateModelPolicy returns a function, that', () => { const result = await buildCanCreateModelPolicy({})(canCreateArgs()) expect(result).toBeAuthOKResult() }) + + it('allows even if workspaceId is set, but workspace module is disabled', async () => { + const result = await buildCanCreateModelPolicy({ + getWorkspace: async () => { + assert.fail() + }, + getWorkspaceRole: async () => { + assert.fail() + }, + getEnv: async () => + parseFeatureFlags({ + FF_WORKSPACES_MODULE_ENABLED: 'false', + FF_PERSONAL_PROJECTS_LIMITS_ENABLED: 'false' + }) + })(canCreateArgs()) + + expect(result).toBeAuthOKResult() + }) }) diff --git a/packages/shared/src/authz/policies/project/model/canCreate.ts b/packages/shared/src/authz/policies/project/model/canCreate.ts index 03f637a05..86c46b7cf 100644 --- a/packages/shared/src/authz/policies/project/model/canCreate.ts +++ b/packages/shared/src/authz/policies/project/model/canCreate.ts @@ -77,16 +77,5 @@ export const canCreateModelPolicy: AuthPolicy< return err(ensuredModelsAccepted.error) } - // Prevent personal project models, if personal projects limited - const project = await loaders.getProject({ projectId }) - const env = await loaders.getEnv() - if (project && !project.workspaceId && env.FF_PERSONAL_PROJECTS_LIMITS_ENABLED) { - return err( - new PersonalProjectsLimitedError( - 'No new models can be added to personal projects' - ) - ) - } - return ok() }