fix(fe2): cors OPTIONS reqs sometimes returning redirects (#4860)
* fix(fe2): cors OPTIONS reqs sometimes returning redirects * path parsing fix * test fix
This commit is contained in:
committed by
GitHub
parent
23b61769b7
commit
4412e7a798
@@ -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: '/',
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import {
|
||||
PersonalProjectsLimitedError,
|
||||
ProjectNoAccessError,
|
||||
ProjectNotEnoughPermissionsError,
|
||||
ProjectNotFoundError,
|
||||
@@ -65,6 +66,7 @@ type PolicyErrors =
|
||||
| InstanceType<typeof WorkspaceNoEditorSeatError>
|
||||
| InstanceType<typeof WorkspaceNotEnoughPermissionsError>
|
||||
| InstanceType<typeof ProjectNotEnoughPermissionsError>
|
||||
| InstanceType<typeof PersonalProjectsLimitedError>
|
||||
|
||||
export const canMoveToWorkspacePolicy: AuthPolicy<
|
||||
PolicyLoaderKeys,
|
||||
|
||||
@@ -21,7 +21,11 @@ const buildCanCreateModelPolicy = (
|
||||
overrides?: Partial<Parameters<typeof canCreateModelPolicy>[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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user