diff --git a/packages/frontend-2/lib/auth/composables/authPolicies.ts b/packages/frontend-2/lib/auth/composables/authPolicies.ts deleted file mode 100644 index 42d5a673c..000000000 --- a/packages/frontend-2/lib/auth/composables/authPolicies.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const useAuthPolicies = () => { - const nuxt = useNuxtApp() - return nuxt.$authPolicies -} diff --git a/packages/frontend-2/lib/auth/graphql/fragments.ts b/packages/frontend-2/lib/auth/graphql/fragments.ts new file mode 100644 index 000000000..639886783 --- /dev/null +++ b/packages/frontend-2/lib/auth/graphql/fragments.ts @@ -0,0 +1,10 @@ +import { graphql } from '~/lib/common/generated/gql' + +export const permissionCheckResultFragment = graphql(` + fragment FullPermissionCheckResult on PermissionCheckResult { + authorized + code + message + payload + } +`) diff --git a/packages/frontend-2/lib/auth/loaders/env.ts b/packages/frontend-2/lib/auth/loaders/env.ts deleted file mode 100644 index 4b412a092..000000000 --- a/packages/frontend-2/lib/auth/loaders/env.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { AuthCheckContextLoaders } from '@speckle/shared/authz' -import type { AuthLoaderFactory } from '~/lib/auth/helpers/authPolicies' - -export const getEnvFactory: AuthLoaderFactory = ( - deps -) => { - const { public: publicRuntimeConfig } = deps.nuxtApp.$config - return async () => publicRuntimeConfig -} diff --git a/packages/frontend-2/lib/auth/loaders/index.ts b/packages/frontend-2/lib/auth/loaders/index.ts deleted file mode 100644 index 549e9eb10..000000000 --- a/packages/frontend-2/lib/auth/loaders/index.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import type { NuxtApp } from '#app' -import type { - DocumentNode, - OperationVariables, - QueryOptions -} from '@apollo/client/core' -import type { Authz } from '@speckle/shared' -import type { AuthLoaderDependencies } from '~/lib/auth/helpers/authPolicies' -import { getEnvFactory } from '~/lib/auth/loaders/env' -import { getProjectFactory, getProjectRoleFactory } from '~/lib/auth/loaders/project' -import { getServerRoleFactory } from '~/lib/auth/loaders/server' -import { - getWorkspaceFactory, - getWorkspaceRoleFactory, - getWorkspaceSsoProviderFactory, - getWorkspaceSsoSessionFactory -} from '~/lib/auth/loaders/workspace' - -export const buildAuthPolicyLoaders = (params: { - nuxtApp: NuxtApp - options?: Partial<{ - /** - * Whether loaders should skip cache and fetch results from server - */ - noCache: boolean - }> -}): Authz.AllAuthCheckContextLoaders => { - const apollo = params.nuxtApp['$apollo'].default - if (!apollo) { - throw new Error('Apollo client not found') - } - - const requestedQueryMap: WeakMap> = new WeakMap() - - const query: (typeof apollo)['query'] = < - T = any, - TVariables extends OperationVariables = OperationVariables - >( - options: QueryOptions - ) => { - const op = options.query - const vars = JSON.stringify(toValue(options.variables)) - - // If noCache - we want fetchPolicy: network-only ONLY on the first load of a specific - // gql query. network-only on subsequent loads within the same policy will be too heavy - if (params.options?.noCache && !options.fetchPolicy) { - if (!requestedQueryMap.has(op)) { - requestedQueryMap.set(op, new Set()) - } - - const queryMap = requestedQueryMap.get(op)! - if (!queryMap.has(vars)) { - queryMap.add(vars) - options.fetchPolicy = 'network-only' - } else { - options.fetchPolicy = 'cache-first' - } - } - - return apollo.query(options) - } - - const deps: AuthLoaderDependencies = { - nuxtApp: params.nuxtApp, - query - } - - return { - getEnv: getEnvFactory(deps), - getProject: getProjectFactory(deps), - getProjectRole: getProjectRoleFactory(deps), - getServerRole: getServerRoleFactory(deps), - getWorkspace: getWorkspaceFactory(deps), - getWorkspaceRole: getWorkspaceRoleFactory(deps), - getWorkspaceSsoProvider: getWorkspaceSsoProviderFactory(deps), - getWorkspaceSsoSession: getWorkspaceSsoSessionFactory(deps) - } -} diff --git a/packages/frontend-2/lib/auth/loaders/project.ts b/packages/frontend-2/lib/auth/loaders/project.ts deleted file mode 100644 index 8a6e90edc..000000000 --- a/packages/frontend-2/lib/auth/loaders/project.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { - ProjectNoAccessError, - ProjectNotFoundError, - ProjectRoleNotFoundError, - WorkspaceSsoSessionNoAccessError, - type AuthCheckContextLoaders -} from '@speckle/shared/authz' -import { err, ok } from 'true-myth/result' -import { graphql } from '~/lib/common/generated/gql' -import { SimpleProjectVisibility } from '~/lib/common/generated/gql/graphql' -import { hasErrorWith } from '~/lib/common/helpers/graphql' -import { WorkspaceSsoErrorCodes } from '~/lib/workspaces/helpers/types' -import type { StreamRoles } from '@speckle/shared' -import { ActiveUserId, type AuthLoaderFactory } from '~/lib/auth/helpers/authPolicies' - -const ProjectErrorCodes = { - NotFound: 'STREAM_NOT_FOUND', - Forbidden: 'FORBIDDEN', - SsoSessionError: WorkspaceSsoErrorCodes.SESSION_MISSING_OR_EXPIRED -} - -// Re-using same query for multiple checks for optimal performance -const authzProjectMetadataQuery = graphql(` - query AuthzProjectMetadata($id: String!) { - project(id: $id) { - id - ...AuthzGetProject_Project - ...AuthzGetProjectRole_Project - } - } -`) - -graphql(` - fragment AuthzGetProject_Project on Project { - id - visibility - workspaceId - } -`) - -export const getProjectFactory: AuthLoaderFactory< - AuthCheckContextLoaders['getProject'] -> = (deps) => { - return async ({ projectId }) => { - const { data, errors } = await deps - .query({ - query: authzProjectMetadataQuery, - variables: { id: projectId } - }) - .catch(convertThrowIntoFetchResult) - - const isSsoSessionError = hasErrorWith({ - errors, - code: ProjectErrorCodes.SsoSessionError - }) - if (isSsoSessionError) - return err( - new WorkspaceSsoSessionNoAccessError({ - payload: { - workspaceSlug: isSsoSessionError.message - } - }) - ) - - const isNotFound = hasErrorWith({ errors, code: ProjectErrorCodes.NotFound }) - if (isNotFound) return err(new ProjectNotFoundError()) - - const isForbidden = hasErrorWith({ - errors, - code: ProjectErrorCodes.Forbidden - }) - if (isForbidden) return err(new ProjectNoAccessError()) - - if (data?.project.id) - return ok({ - id: data.project.id, - isDiscoverable: false, - isPublic: data.project.visibility === SimpleProjectVisibility.Unlisted, - workspaceId: data.project.workspaceId || null - }) - - throw new Error("Couldn't retrieve project due to unexpected error") - } -} - -graphql(` - fragment AuthzGetProjectRole_Project on Project { - id - role - } -`) - -export const getProjectRoleFactory: AuthLoaderFactory< - AuthCheckContextLoaders['getProjectRole'] -> = (deps) => { - const { userId: activeUserId } = useActiveUser() - - return async ({ projectId, userId }) => { - if (userId !== activeUserId.value && userId !== ActiveUserId) { - throw new Error('Checking project role for a different user is not supported') - } - - const { data, errors } = await deps - .query({ - query: authzProjectMetadataQuery, - variables: { id: projectId } - }) - .catch(convertThrowIntoFetchResult) - - const hasExpectedNotFoundErrors = hasErrorWith({ - errors, - codes: [ - ProjectErrorCodes.NotFound, - ProjectErrorCodes.Forbidden, - ProjectErrorCodes.SsoSessionError - ] - }) - if (hasExpectedNotFoundErrors) return err(new ProjectRoleNotFoundError()) - - if (data?.project.id) { - return data.project.role - ? ok(data.project.role as StreamRoles) - : err(new ProjectRoleNotFoundError()) - } - - throw new Error("Couldn't retrieve project role due to unexpected error") - } -} diff --git a/packages/frontend-2/lib/auth/loaders/server.ts b/packages/frontend-2/lib/auth/loaders/server.ts deleted file mode 100644 index e07c2f6a5..000000000 --- a/packages/frontend-2/lib/auth/loaders/server.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { - ServerRoleNotFoundError, - type AuthCheckContextLoaders -} from '@speckle/shared/authz' -import type { ServerRoles } from '@speckle/shared' -import { err, ok } from 'true-myth/result' -import { graphql } from '~/lib/common/generated/gql' -import { ActiveUserId, type AuthLoaderFactory } from '~/lib/auth/helpers/authPolicies' - -const authzServerMetadataQuery = graphql(` - query AuthzServerMetadata { - activeUser { - id - ...AuthzGetServerRole_User - } - } -`) - -graphql(` - fragment AuthzGetServerRole_User on User { - id - role - } -`) - -export const getServerRoleFactory: AuthLoaderFactory< - AuthCheckContextLoaders['getServerRole'] -> = (deps) => { - const { userId: activeUserId } = useActiveUser() - - return async ({ userId }) => { - if (userId !== activeUserId.value && userId !== ActiveUserId) { - throw new Error('Checking server role for another user is not supported') - } - - const { data, errors } = await deps - .query({ - query: authzServerMetadataQuery, - // We're fine with always using the cache for this, it's very unlikely that user's role will change - // and if it does it's definitely gonna cause a problem elsewhere - fetchPolicy: 'cache-first' - }) - .catch(convertThrowIntoFetchResult) - if (errors?.length) { - throw new Error('Failed to load server role') - } - - return data?.activeUser?.role - ? ok(data.activeUser.role as ServerRoles) - : err(new ServerRoleNotFoundError()) - } -} diff --git a/packages/frontend-2/lib/auth/loaders/workspace.ts b/packages/frontend-2/lib/auth/loaders/workspace.ts deleted file mode 100644 index b9c0cec61..000000000 --- a/packages/frontend-2/lib/auth/loaders/workspace.ts +++ /dev/null @@ -1,205 +0,0 @@ -import type { WorkspaceRoles } from '@speckle/shared' -import { - WorkspaceNoAccessError, - WorkspaceNotFoundError, - WorkspaceRoleNotFoundError, - WorkspaceSsoProviderNotFoundError, - WorkspaceSsoSessionNoAccessError, - WorkspaceSsoSessionNotFoundError, - type AuthCheckContextLoaders -} from '@speckle/shared/authz' -import dayjs from 'dayjs' -import { err, ok } from 'true-myth/result' -import { ActiveUserId, type AuthLoaderFactory } from '~/lib/auth/helpers/authPolicies' -import { graphql } from '~/lib/common/generated/gql' -import { hasErrorWith } from '~/lib/common/helpers/graphql' -import { WorkspaceSsoErrorCodes } from '~/lib/workspaces/helpers/types' - -const WorkspaceErrorCodes = { - NotFound: 'WORKSPACE_NOT_FOUND_ERROR', - Forbidden: 'FORBIDDEN', - SsoSessionError: WorkspaceSsoErrorCodes.SESSION_MISSING_OR_EXPIRED -} - -const authzWorkspaceMetadataQuery = graphql(` - query AuthzWorkspaceMetadata($id: String!) { - workspace(id: $id) { - id - ...AuthzGetWorkspace_Workspace - ...AuthzGetWorkspaceRole_Workspace - ...AuthzGetWorkspaceSsoProviderSession_Workspace - } - } -`) - -graphql(` - fragment AuthzGetWorkspace_Workspace on Workspace { - id - slug - } -`) - -export const getWorkspaceFactory: AuthLoaderFactory< - AuthCheckContextLoaders['getWorkspace'] -> = (deps) => { - return async ({ workspaceId }) => { - const { data, errors } = await deps - .query({ - query: authzWorkspaceMetadataQuery, - variables: { id: workspaceId } - }) - .catch(convertThrowIntoFetchResult) - - const isSsoSessionError = hasErrorWith({ - errors, - code: WorkspaceErrorCodes.SsoSessionError - }) - if (isSsoSessionError) - return err( - new WorkspaceSsoSessionNoAccessError({ - payload: { - workspaceSlug: isSsoSessionError.message - } - }) - ) - - const isNotFound = hasErrorWith({ errors, code: WorkspaceErrorCodes.NotFound }) - if (isNotFound) return err(new WorkspaceNotFoundError()) - - const isForbidden = hasErrorWith({ - errors, - code: WorkspaceErrorCodes.Forbidden - }) - if (isForbidden) return err(new WorkspaceNoAccessError()) - - if (data?.workspace.id) return ok(data.workspace) - - throw new Error('Unexpectedly failed to load workspace') - } -} - -graphql(` - fragment AuthzGetWorkspaceRole_Workspace on Workspace { - id - role - } -`) - -export const getWorkspaceRoleFactory: AuthLoaderFactory< - AuthCheckContextLoaders['getWorkspaceRole'] -> = (deps) => { - const { userId: activeUserId } = useActiveUser() - - return async ({ workspaceId, userId }) => { - if (userId !== activeUserId.value && userId !== ActiveUserId) { - throw new Error('Checking workspace role for another user is not supported') - } - - const { data, errors } = await deps - .query({ - query: authzWorkspaceMetadataQuery, - variables: { id: workspaceId } - }) - .catch(convertThrowIntoFetchResult) - - const hasExpectedNotFoundErrors = hasErrorWith({ - errors, - codes: [ - WorkspaceErrorCodes.NotFound, - WorkspaceErrorCodes.Forbidden, - WorkspaceErrorCodes.SsoSessionError - ] - }) - if (hasExpectedNotFoundErrors) return err(new WorkspaceRoleNotFoundError()) - if (data?.workspace.role) { - return ok(data.workspace.role as WorkspaceRoles) - } - - throw new Error("Couldn't retrieve project role due to unexpected error") - } -} - -graphql(` - fragment AuthzGetWorkspaceSsoProviderSession_Workspace on Workspace { - id - sso { - provider { - id - } - session { - validUntil - } - } - } -`) - -export const getWorkspaceSsoProviderFactory: AuthLoaderFactory< - AuthCheckContextLoaders['getWorkspaceSsoProvider'] -> = (deps) => { - return async ({ workspaceId }) => { - const { data, errors } = await deps - .query({ - query: authzWorkspaceMetadataQuery, - variables: { id: workspaceId } - }) - .catch(convertThrowIntoFetchResult) - - const hasExpectedNotFoundErrors = hasErrorWith({ - errors, - codes: [ - WorkspaceErrorCodes.NotFound, - WorkspaceErrorCodes.Forbidden, - WorkspaceErrorCodes.SsoSessionError - ] - }) - if (hasExpectedNotFoundErrors) return err(new WorkspaceSsoProviderNotFoundError()) - if (errors?.length) { - throw new Error("Couldn't retrieve project role due to unexpected error") - } - - return data?.workspace.sso?.provider - ? ok({ providerId: data.workspace.sso.provider.id }) - : err(new WorkspaceSsoProviderNotFoundError()) - } -} - -export const getWorkspaceSsoSessionFactory: AuthLoaderFactory< - AuthCheckContextLoaders['getWorkspaceSsoSession'] -> = (deps) => { - const { userId: activeUserId } = useActiveUser() - - return async ({ workspaceId, userId }) => { - if (userId !== activeUserId.value && userId !== ActiveUserId) { - throw new Error('Checking workspace session for another user is not supported') - } - if (!activeUserId.value) return err(new WorkspaceSsoSessionNotFoundError()) - - const { data, errors } = await deps - .query({ - query: authzWorkspaceMetadataQuery, - variables: { id: workspaceId } - }) - .catch(convertThrowIntoFetchResult) - - const hasExpectedNotFoundErrors = hasErrorWith({ - errors, - codes: [ - WorkspaceErrorCodes.NotFound, - WorkspaceErrorCodes.Forbidden, - WorkspaceErrorCodes.SsoSessionError - ] - }) - if (hasExpectedNotFoundErrors) return err(new WorkspaceSsoSessionNotFoundError()) - if (errors?.length) { - throw new Error("Couldn't retrieve project role due to unexpected error") - } - - return data?.workspace.sso?.session && data.workspace.sso.provider - ? ok({ - providerId: data.workspace.sso.provider.id, - userId: activeUserId.value, - validUntil: dayjs(data.workspace.sso.session.validUntil).toDate() - }) - : err(new WorkspaceSsoSessionNotFoundError()) - } -} diff --git a/packages/frontend-2/lib/common/generated/gql/gql.ts b/packages/frontend-2/lib/common/generated/gql/gql.ts index 1cb24e9fe..0f01f5e51 100644 --- a/packages/frontend-2/lib/common/generated/gql/gql.ts +++ b/packages/frontend-2/lib/common/generated/gql/gql.ts @@ -161,6 +161,7 @@ type Documents = { "\n fragment WorkspaceWizardStepRegion_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": typeof types.WorkspaceWizardStepRegion_ServerInfoFragmentDoc, "\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n emails {\n id\n verified\n }\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n versions(limit: 0) {\n totalCount\n }\n }\n }\n": typeof types.ActiveUserMainMetadataDocument, "\n mutation CreateOnboardingProject {\n projectMutations {\n createForOnboarding {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n ": typeof types.CreateOnboardingProjectDocument, + "\n fragment FullPermissionCheckResult on PermissionCheckResult {\n authorized\n code\n message\n payload\n }\n": typeof types.FullPermissionCheckResultFragmentDoc, "\n mutation FinishOnboarding($input: OnboardingCompletionInput) {\n activeUserMutations {\n finishOnboarding(input: $input)\n }\n }\n": typeof types.FinishOnboardingDocument, "\n mutation RequestVerificationByEmail($email: String!) {\n requestVerificationByEmail(email: $email)\n }\n": typeof types.RequestVerificationByEmailDocument, "\n query AuthLoginPanel {\n serverInfo {\n authStrategies {\n id\n }\n ...AuthStategiesServerInfoFragment\n }\n }\n": typeof types.AuthLoginPanelDocument, @@ -170,15 +171,6 @@ type Documents = { "\n query ActiveUserWorkspaceExistenceCheck {\n activeUser {\n id\n verified\n isOnboardingFinished\n versions(limit: 0) {\n totalCount\n }\n workspaces(limit: 0) {\n totalCount\n items {\n id\n slug\n }\n }\n discoverableWorkspaces {\n id\n }\n workspaceJoinRequests(limit: 0) {\n totalCount\n }\n }\n }\n": typeof types.ActiveUserWorkspaceExistenceCheckDocument, "\n query ActiveUserActiveWorkspaceCheck {\n activeUser {\n id\n isProjectsActive\n activeWorkspace {\n id\n slug\n }\n }\n }\n": typeof types.ActiveUserActiveWorkspaceCheckDocument, "\n query projectWorkspaceAccessCheck($projectId: String!) {\n project(id: $projectId) {\n id\n role\n workspace {\n id\n slug\n role\n }\n }\n }\n": typeof types.ProjectWorkspaceAccessCheckDocument, - "\n query AuthzProjectMetadata($id: String!) {\n project(id: $id) {\n id\n ...AuthzGetProject_Project\n ...AuthzGetProjectRole_Project\n }\n }\n": typeof types.AuthzProjectMetadataDocument, - "\n fragment AuthzGetProject_Project on Project {\n id\n visibility\n workspaceId\n }\n": typeof types.AuthzGetProject_ProjectFragmentDoc, - "\n fragment AuthzGetProjectRole_Project on Project {\n id\n role\n }\n": typeof types.AuthzGetProjectRole_ProjectFragmentDoc, - "\n query AuthzServerMetadata {\n activeUser {\n id\n ...AuthzGetServerRole_User\n }\n }\n": typeof types.AuthzServerMetadataDocument, - "\n fragment AuthzGetServerRole_User on User {\n id\n role\n }\n": typeof types.AuthzGetServerRole_UserFragmentDoc, - "\n query AuthzWorkspaceMetadata($id: String!) {\n workspace(id: $id) {\n id\n ...AuthzGetWorkspace_Workspace\n ...AuthzGetWorkspaceRole_Workspace\n ...AuthzGetWorkspaceSsoProviderSession_Workspace\n }\n }\n": typeof types.AuthzWorkspaceMetadataDocument, - "\n fragment AuthzGetWorkspace_Workspace on Workspace {\n id\n slug\n }\n": typeof types.AuthzGetWorkspace_WorkspaceFragmentDoc, - "\n fragment AuthzGetWorkspaceRole_Workspace on Workspace {\n id\n role\n }\n": typeof types.AuthzGetWorkspaceRole_WorkspaceFragmentDoc, - "\n fragment AuthzGetWorkspaceSsoProviderSession_Workspace on Workspace {\n id\n sso {\n provider {\n id\n }\n session {\n validUntil\n }\n }\n }\n": typeof types.AuthzGetWorkspaceSsoProviderSession_WorkspaceFragmentDoc, "\n fragment FunctionRunStatusForSummary on AutomateFunctionRun {\n id\n status\n }\n": typeof types.FunctionRunStatusForSummaryFragmentDoc, "\n fragment TriggeredAutomationsStatusSummary on TriggeredAutomationsStatus {\n id\n automationRuns {\n id\n functionRuns {\n id\n ...FunctionRunStatusForSummary\n }\n }\n }\n": typeof types.TriggeredAutomationsStatusSummaryFragmentDoc, "\n fragment AutomationRunDetails on AutomateRun {\n id\n status\n functionRuns {\n ...FunctionRunStatusForSummary\n statusMessage\n }\n trigger {\n ... on VersionCreatedTrigger {\n version {\n id\n }\n model {\n id\n }\n }\n }\n createdAt\n updatedAt\n }\n": typeof types.AutomationRunDetailsFragmentDoc, @@ -266,7 +258,7 @@ type Documents = { "\n mutation TriggerAutomation($projectId: ID!, $automationId: ID!) {\n projectMutations {\n automationMutations(projectId: $projectId) {\n trigger(automationId: $automationId)\n }\n }\n }\n": typeof types.TriggerAutomationDocument, "\n mutation CreateTestAutomation(\n $projectId: ID!\n $input: ProjectTestAutomationCreateInput!\n ) {\n projectMutations {\n automationMutations(projectId: $projectId) {\n createTestAutomation(input: $input) {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n }\n }\n }\n": typeof types.CreateTestAutomationDocument, "\n mutation MoveProjectToWorkspace($workspaceId: String!, $projectId: String!) {\n workspaceMutations {\n projects {\n moveToWorkspace(workspaceId: $workspaceId, projectId: $projectId) {\n id\n workspace {\n id\n projects {\n items {\n id\n }\n }\n ...ProjectsMoveToWorkspaceDialog_Workspace\n ...MoveProjectsDialog_Workspace\n }\n }\n }\n }\n }\n": typeof types.MoveProjectToWorkspaceDocument, - "\n query ProjectAccessCheck($id: String!) {\n project(id: $id) {\n id\n visibility\n workspace {\n id\n slug\n }\n }\n }\n": typeof types.ProjectAccessCheckDocument, + "\n query ProjectAccessCheck($id: String!) {\n project(id: $id) {\n id\n permissions {\n canRead {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n": typeof types.ProjectAccessCheckDocument, "\n query ProjectRoleCheck($id: String!) {\n project(id: $id) {\n id\n role\n }\n }\n": typeof types.ProjectRoleCheckDocument, "\n query ProjectsDashboardQuery($filter: UserProjectsFilter, $cursor: String) {\n activeUser {\n id\n projects(filter: $filter, limit: 6, cursor: $cursor) {\n ...ProjectsDashboard_UserProjectCollection\n cursor\n totalCount\n items {\n ...ProjectDashboardItem\n }\n }\n ...ProjectsHiddenProjectWarning_User\n ...ProjectsDashboardHeaderProjects_User\n }\n }\n": typeof types.ProjectsDashboardQueryDocument, "\n query ProjectsDashboardWorkspaceQuery {\n activeUser {\n id\n ...ProjectsDashboardHeaderWorkspaces_User\n }\n }\n": typeof types.ProjectsDashboardWorkspaceQueryDocument, @@ -583,6 +575,7 @@ const documents: Documents = { "\n fragment WorkspaceWizardStepRegion_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": types.WorkspaceWizardStepRegion_ServerInfoFragmentDoc, "\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n emails {\n id\n verified\n }\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n versions(limit: 0) {\n totalCount\n }\n }\n }\n": types.ActiveUserMainMetadataDocument, "\n mutation CreateOnboardingProject {\n projectMutations {\n createForOnboarding {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n ": types.CreateOnboardingProjectDocument, + "\n fragment FullPermissionCheckResult on PermissionCheckResult {\n authorized\n code\n message\n payload\n }\n": types.FullPermissionCheckResultFragmentDoc, "\n mutation FinishOnboarding($input: OnboardingCompletionInput) {\n activeUserMutations {\n finishOnboarding(input: $input)\n }\n }\n": types.FinishOnboardingDocument, "\n mutation RequestVerificationByEmail($email: String!) {\n requestVerificationByEmail(email: $email)\n }\n": types.RequestVerificationByEmailDocument, "\n query AuthLoginPanel {\n serverInfo {\n authStrategies {\n id\n }\n ...AuthStategiesServerInfoFragment\n }\n }\n": types.AuthLoginPanelDocument, @@ -592,15 +585,6 @@ const documents: Documents = { "\n query ActiveUserWorkspaceExistenceCheck {\n activeUser {\n id\n verified\n isOnboardingFinished\n versions(limit: 0) {\n totalCount\n }\n workspaces(limit: 0) {\n totalCount\n items {\n id\n slug\n }\n }\n discoverableWorkspaces {\n id\n }\n workspaceJoinRequests(limit: 0) {\n totalCount\n }\n }\n }\n": types.ActiveUserWorkspaceExistenceCheckDocument, "\n query ActiveUserActiveWorkspaceCheck {\n activeUser {\n id\n isProjectsActive\n activeWorkspace {\n id\n slug\n }\n }\n }\n": types.ActiveUserActiveWorkspaceCheckDocument, "\n query projectWorkspaceAccessCheck($projectId: String!) {\n project(id: $projectId) {\n id\n role\n workspace {\n id\n slug\n role\n }\n }\n }\n": types.ProjectWorkspaceAccessCheckDocument, - "\n query AuthzProjectMetadata($id: String!) {\n project(id: $id) {\n id\n ...AuthzGetProject_Project\n ...AuthzGetProjectRole_Project\n }\n }\n": types.AuthzProjectMetadataDocument, - "\n fragment AuthzGetProject_Project on Project {\n id\n visibility\n workspaceId\n }\n": types.AuthzGetProject_ProjectFragmentDoc, - "\n fragment AuthzGetProjectRole_Project on Project {\n id\n role\n }\n": types.AuthzGetProjectRole_ProjectFragmentDoc, - "\n query AuthzServerMetadata {\n activeUser {\n id\n ...AuthzGetServerRole_User\n }\n }\n": types.AuthzServerMetadataDocument, - "\n fragment AuthzGetServerRole_User on User {\n id\n role\n }\n": types.AuthzGetServerRole_UserFragmentDoc, - "\n query AuthzWorkspaceMetadata($id: String!) {\n workspace(id: $id) {\n id\n ...AuthzGetWorkspace_Workspace\n ...AuthzGetWorkspaceRole_Workspace\n ...AuthzGetWorkspaceSsoProviderSession_Workspace\n }\n }\n": types.AuthzWorkspaceMetadataDocument, - "\n fragment AuthzGetWorkspace_Workspace on Workspace {\n id\n slug\n }\n": types.AuthzGetWorkspace_WorkspaceFragmentDoc, - "\n fragment AuthzGetWorkspaceRole_Workspace on Workspace {\n id\n role\n }\n": types.AuthzGetWorkspaceRole_WorkspaceFragmentDoc, - "\n fragment AuthzGetWorkspaceSsoProviderSession_Workspace on Workspace {\n id\n sso {\n provider {\n id\n }\n session {\n validUntil\n }\n }\n }\n": types.AuthzGetWorkspaceSsoProviderSession_WorkspaceFragmentDoc, "\n fragment FunctionRunStatusForSummary on AutomateFunctionRun {\n id\n status\n }\n": types.FunctionRunStatusForSummaryFragmentDoc, "\n fragment TriggeredAutomationsStatusSummary on TriggeredAutomationsStatus {\n id\n automationRuns {\n id\n functionRuns {\n id\n ...FunctionRunStatusForSummary\n }\n }\n }\n": types.TriggeredAutomationsStatusSummaryFragmentDoc, "\n fragment AutomationRunDetails on AutomateRun {\n id\n status\n functionRuns {\n ...FunctionRunStatusForSummary\n statusMessage\n }\n trigger {\n ... on VersionCreatedTrigger {\n version {\n id\n }\n model {\n id\n }\n }\n }\n createdAt\n updatedAt\n }\n": types.AutomationRunDetailsFragmentDoc, @@ -688,7 +672,7 @@ const documents: Documents = { "\n mutation TriggerAutomation($projectId: ID!, $automationId: ID!) {\n projectMutations {\n automationMutations(projectId: $projectId) {\n trigger(automationId: $automationId)\n }\n }\n }\n": types.TriggerAutomationDocument, "\n mutation CreateTestAutomation(\n $projectId: ID!\n $input: ProjectTestAutomationCreateInput!\n ) {\n projectMutations {\n automationMutations(projectId: $projectId) {\n createTestAutomation(input: $input) {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n }\n }\n }\n": types.CreateTestAutomationDocument, "\n mutation MoveProjectToWorkspace($workspaceId: String!, $projectId: String!) {\n workspaceMutations {\n projects {\n moveToWorkspace(workspaceId: $workspaceId, projectId: $projectId) {\n id\n workspace {\n id\n projects {\n items {\n id\n }\n }\n ...ProjectsMoveToWorkspaceDialog_Workspace\n ...MoveProjectsDialog_Workspace\n }\n }\n }\n }\n }\n": types.MoveProjectToWorkspaceDocument, - "\n query ProjectAccessCheck($id: String!) {\n project(id: $id) {\n id\n visibility\n workspace {\n id\n slug\n }\n }\n }\n": types.ProjectAccessCheckDocument, + "\n query ProjectAccessCheck($id: String!) {\n project(id: $id) {\n id\n permissions {\n canRead {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n": types.ProjectAccessCheckDocument, "\n query ProjectRoleCheck($id: String!) {\n project(id: $id) {\n id\n role\n }\n }\n": types.ProjectRoleCheckDocument, "\n query ProjectsDashboardQuery($filter: UserProjectsFilter, $cursor: String) {\n activeUser {\n id\n projects(filter: $filter, limit: 6, cursor: $cursor) {\n ...ProjectsDashboard_UserProjectCollection\n cursor\n totalCount\n items {\n ...ProjectDashboardItem\n }\n }\n ...ProjectsHiddenProjectWarning_User\n ...ProjectsDashboardHeaderProjects_User\n }\n }\n": types.ProjectsDashboardQueryDocument, "\n query ProjectsDashboardWorkspaceQuery {\n activeUser {\n id\n ...ProjectsDashboardHeaderWorkspaces_User\n }\n }\n": types.ProjectsDashboardWorkspaceQueryDocument, @@ -1460,6 +1444,10 @@ export function graphql(source: "\n query ActiveUserMainMetadata {\n activeU * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "\n mutation CreateOnboardingProject {\n projectMutations {\n createForOnboarding {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n "): (typeof documents)["\n mutation CreateOnboardingProject {\n projectMutations {\n createForOnboarding {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n "]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n fragment FullPermissionCheckResult on PermissionCheckResult {\n authorized\n code\n message\n payload\n }\n"): (typeof documents)["\n fragment FullPermissionCheckResult on PermissionCheckResult {\n authorized\n code\n message\n payload\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -1496,42 +1484,6 @@ export function graphql(source: "\n query ActiveUserActiveWorkspaceCheck {\n * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "\n query projectWorkspaceAccessCheck($projectId: String!) {\n project(id: $projectId) {\n id\n role\n workspace {\n id\n slug\n role\n }\n }\n }\n"): (typeof documents)["\n query projectWorkspaceAccessCheck($projectId: String!) {\n project(id: $projectId) {\n id\n role\n workspace {\n id\n slug\n role\n }\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n query AuthzProjectMetadata($id: String!) {\n project(id: $id) {\n id\n ...AuthzGetProject_Project\n ...AuthzGetProjectRole_Project\n }\n }\n"): (typeof documents)["\n query AuthzProjectMetadata($id: String!) {\n project(id: $id) {\n id\n ...AuthzGetProject_Project\n ...AuthzGetProjectRole_Project\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n fragment AuthzGetProject_Project on Project {\n id\n visibility\n workspaceId\n }\n"): (typeof documents)["\n fragment AuthzGetProject_Project on Project {\n id\n visibility\n workspaceId\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n fragment AuthzGetProjectRole_Project on Project {\n id\n role\n }\n"): (typeof documents)["\n fragment AuthzGetProjectRole_Project on Project {\n id\n role\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n query AuthzServerMetadata {\n activeUser {\n id\n ...AuthzGetServerRole_User\n }\n }\n"): (typeof documents)["\n query AuthzServerMetadata {\n activeUser {\n id\n ...AuthzGetServerRole_User\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n fragment AuthzGetServerRole_User on User {\n id\n role\n }\n"): (typeof documents)["\n fragment AuthzGetServerRole_User on User {\n id\n role\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n query AuthzWorkspaceMetadata($id: String!) {\n workspace(id: $id) {\n id\n ...AuthzGetWorkspace_Workspace\n ...AuthzGetWorkspaceRole_Workspace\n ...AuthzGetWorkspaceSsoProviderSession_Workspace\n }\n }\n"): (typeof documents)["\n query AuthzWorkspaceMetadata($id: String!) {\n workspace(id: $id) {\n id\n ...AuthzGetWorkspace_Workspace\n ...AuthzGetWorkspaceRole_Workspace\n ...AuthzGetWorkspaceSsoProviderSession_Workspace\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n fragment AuthzGetWorkspace_Workspace on Workspace {\n id\n slug\n }\n"): (typeof documents)["\n fragment AuthzGetWorkspace_Workspace on Workspace {\n id\n slug\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n fragment AuthzGetWorkspaceRole_Workspace on Workspace {\n id\n role\n }\n"): (typeof documents)["\n fragment AuthzGetWorkspaceRole_Workspace on Workspace {\n id\n role\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n fragment AuthzGetWorkspaceSsoProviderSession_Workspace on Workspace {\n id\n sso {\n provider {\n id\n }\n session {\n validUntil\n }\n }\n }\n"): (typeof documents)["\n fragment AuthzGetWorkspaceSsoProviderSession_Workspace on Workspace {\n id\n sso {\n provider {\n id\n }\n session {\n validUntil\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -1883,7 +1835,7 @@ export function graphql(source: "\n mutation MoveProjectToWorkspace($workspaceI /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query ProjectAccessCheck($id: String!) {\n project(id: $id) {\n id\n visibility\n workspace {\n id\n slug\n }\n }\n }\n"): (typeof documents)["\n query ProjectAccessCheck($id: String!) {\n project(id: $id) {\n id\n visibility\n workspace {\n id\n slug\n }\n }\n }\n"]; +export function graphql(source: "\n query ProjectAccessCheck($id: String!) {\n project(id: $id) {\n id\n permissions {\n canRead {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n"): (typeof documents)["\n query ProjectAccessCheck($id: String!) {\n project(id: $id) {\n id\n permissions {\n canRead {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/packages/frontend-2/lib/common/generated/gql/graphql.ts b/packages/frontend-2/lib/common/generated/gql/graphql.ts index 938e876f4..439f707d2 100644 --- a/packages/frontend-2/lib/common/generated/gql/graphql.ts +++ b/packages/frontend-2/lib/common/generated/gql/graphql.ts @@ -1961,6 +1961,14 @@ export type PendingWorkspaceCollaboratorsFilter = { search?: InputMaybe; }; +export type PermissionCheckResult = { + __typename?: 'PermissionCheckResult'; + authorized: Scalars['Boolean']['output']; + code: Scalars['String']['output']; + message: Scalars['String']['output']; + payload?: Maybe; +}; + export type Price = { __typename?: 'Price'; amount: Scalars['Float']['output']; @@ -2006,6 +2014,7 @@ export type Project = { pendingAccessRequests?: Maybe>; /** Returns a list models that are being created from a file import */ pendingImportedModels: Array; + permissions: ProjectPermissionChecks; /** Active user's role for this project. `null` if request is not authenticated, or the project is not explicitly shared with you. */ role?: Maybe; /** Source apps used in any models of this project */ @@ -2506,6 +2515,11 @@ export const ProjectPendingVersionsUpdatedMessageType = { } as const; export type ProjectPendingVersionsUpdatedMessageType = typeof ProjectPendingVersionsUpdatedMessageType[keyof typeof ProjectPendingVersionsUpdatedMessageType]; +export type ProjectPermissionChecks = { + __typename?: 'ProjectPermissionChecks'; + canRead: PermissionCheckResult; +}; + export type ProjectRole = { __typename?: 'ProjectRole'; project: Project; @@ -4336,6 +4350,7 @@ export type Workspace = { logo?: Maybe; membersByRole?: Maybe; name: Scalars['String']['output']; + permissions: WorkspacePermissionChecks; plan?: Maybe; projects: ProjectCollection; /** A Workspace is marked as readOnly if its trial period is finished or a paid plan is subscribed but payment has failed */ @@ -4700,6 +4715,11 @@ export const WorkspacePaymentMethod = { } as const; export type WorkspacePaymentMethod = typeof WorkspacePaymentMethod[keyof typeof WorkspacePaymentMethod]; +export type WorkspacePermissionChecks = { + __typename?: 'WorkspacePermissionChecks'; + canCreateProject: PermissionCheckResult; +}; + export type WorkspacePlan = { __typename?: 'WorkspacePlan'; createdAt: Scalars['DateTime']['output']; @@ -5268,6 +5288,8 @@ export type CreateOnboardingProjectMutationVariables = Exact<{ [key: string]: ne export type CreateOnboardingProjectMutation = { __typename?: 'Mutation', projectMutations: { __typename?: 'ProjectMutations', createForOnboarding: { __typename?: 'Project', id: string, createdAt: string, role?: string | null, name: string, description?: string | null, visibility: SimpleProjectVisibility, allowPublicComments: boolean, updatedAt: string, modelCount: { __typename?: 'ModelCollection', totalCount: number }, commentThreadCount: { __typename?: 'ProjectCommentCollection', totalCount: number }, workspace?: { __typename?: 'Workspace', id: string, readOnly: boolean, slug: string, name: string, logo?: string | null } | null, models: { __typename?: 'ModelCollection', totalCount: number, items: Array<{ __typename?: 'Model', id: string, name: string, displayName: string, previewUrl?: string | null, createdAt: string, updatedAt: string, description?: string | null, versionCount: { __typename?: 'VersionCollection', totalCount: number }, commentThreadCount: { __typename?: 'CommentCollection', totalCount: number }, pendingImportedVersions: Array<{ __typename?: 'FileUpload', id: string, projectId: string, modelName: string, convertedStatus: number, convertedMessage?: string | null, uploadDate: string, convertedLastUpdate: string, fileType: string, fileName: string }>, automationsStatus?: { __typename?: 'TriggeredAutomationsStatus', id: string, automationRuns: Array<{ __typename?: 'AutomateRun', id: string, functionRuns: Array<{ __typename?: 'AutomateFunctionRun', id: string, updatedAt: string, status: AutomateRunStatus, results?: {} | null, statusMessage?: string | null, contextView?: string | null, createdAt: string, function?: { __typename?: 'AutomateFunction', id: string, logo?: string | null, name: string } | null }>, automation: { __typename?: 'Automation', id: string, name: string } }> } | null }> }, pendingImportedModels: Array<{ __typename?: 'FileUpload', id: string, projectId: string, modelName: string, convertedStatus: number, convertedMessage?: string | null, uploadDate: string, convertedLastUpdate: string, fileType: string, fileName: string }>, invitedTeam?: Array<{ __typename?: 'PendingStreamCollaborator', id: string, title: string, role: string, inviteId: string, user?: { __typename?: 'LimitedUser', role?: string | null, id: string, name: string, avatar?: string | null } | null }> | null, team: Array<{ __typename?: 'ProjectCollaborator', role: string, seatType?: WorkspaceSeatType | null, id: string, user: { __typename?: 'LimitedUser', role?: string | null, id: string, name: string, avatar?: string | null } }>, versions: { __typename?: 'VersionCollection', totalCount: number } } } }; +export type FullPermissionCheckResultFragment = { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: {} | null }; + export type FinishOnboardingMutationVariables = Exact<{ input?: InputMaybe; }>; @@ -5325,37 +5347,6 @@ export type ProjectWorkspaceAccessCheckQueryVariables = Exact<{ export type ProjectWorkspaceAccessCheckQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, role?: string | null, workspace?: { __typename?: 'Workspace', id: string, slug: string, role?: string | null } | null } }; -export type AuthzProjectMetadataQueryVariables = Exact<{ - id: Scalars['String']['input']; -}>; - - -export type AuthzProjectMetadataQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, visibility: SimpleProjectVisibility, workspaceId?: string | null, role?: string | null } }; - -export type AuthzGetProject_ProjectFragment = { __typename?: 'Project', id: string, visibility: SimpleProjectVisibility, workspaceId?: string | null }; - -export type AuthzGetProjectRole_ProjectFragment = { __typename?: 'Project', id: string, role?: string | null }; - -export type AuthzServerMetadataQueryVariables = Exact<{ [key: string]: never; }>; - - -export type AuthzServerMetadataQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', id: string, role?: string | null } | null }; - -export type AuthzGetServerRole_UserFragment = { __typename?: 'User', id: string, role?: string | null }; - -export type AuthzWorkspaceMetadataQueryVariables = Exact<{ - id: Scalars['String']['input']; -}>; - - -export type AuthzWorkspaceMetadataQuery = { __typename?: 'Query', workspace: { __typename?: 'Workspace', id: string, slug: string, role?: string | null, sso?: { __typename?: 'WorkspaceSso', provider?: { __typename?: 'WorkspaceSsoProvider', id: string } | null, session?: { __typename?: 'WorkspaceSsoSession', validUntil: string } | null } | null } }; - -export type AuthzGetWorkspace_WorkspaceFragment = { __typename?: 'Workspace', id: string, slug: string }; - -export type AuthzGetWorkspaceRole_WorkspaceFragment = { __typename?: 'Workspace', id: string, role?: string | null }; - -export type AuthzGetWorkspaceSsoProviderSession_WorkspaceFragment = { __typename?: 'Workspace', id: string, sso?: { __typename?: 'WorkspaceSso', provider?: { __typename?: 'WorkspaceSsoProvider', id: string } | null, session?: { __typename?: 'WorkspaceSsoSession', validUntil: string } | null } | null }; - export type FunctionRunStatusForSummaryFragment = { __typename?: 'AutomateFunctionRun', id: string, status: AutomateRunStatus }; export type TriggeredAutomationsStatusSummaryFragment = { __typename?: 'TriggeredAutomationsStatus', id: string, automationRuns: Array<{ __typename?: 'AutomateRun', id: string, functionRuns: Array<{ __typename?: 'AutomateFunctionRun', id: string, status: AutomateRunStatus }> }> }; @@ -5881,7 +5872,7 @@ export type ProjectAccessCheckQueryVariables = Exact<{ }>; -export type ProjectAccessCheckQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, visibility: SimpleProjectVisibility, workspace?: { __typename?: 'Workspace', id: string, slug: string } | null } }; +export type ProjectAccessCheckQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, permissions: { __typename?: 'ProjectPermissionChecks', canRead: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: {} | null } } } }; export type ProjectRoleCheckQueryVariables = Exact<{ id: Scalars['String']['input']; @@ -7057,12 +7048,7 @@ export const WorkspaceSidebar_WorkspaceFragmentDoc = {"kind":"Document","definit export const WorkspaceWizard_WorkspaceFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceWizard_Workspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"creationState"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"completed"}},{"kind":"Field","name":{"kind":"Name","value":"state"}}]}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]} as unknown as DocumentNode; export const SettingsWorkspacesRegionsSelect_ServerRegionItemFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsWorkspacesRegionsSelect_ServerRegionItem"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerRegionItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]} as unknown as DocumentNode; export const WorkspaceWizardStepRegion_ServerInfoFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceWizardStepRegion_ServerInfo"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"multiRegion"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"regions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"SettingsWorkspacesRegionsSelect_ServerRegionItem"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsWorkspacesRegionsSelect_ServerRegionItem"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerRegionItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]} as unknown as DocumentNode; -export const AuthzGetProject_ProjectFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AuthzGetProject_Project"},"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":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}}]}}]} as unknown as DocumentNode; -export const AuthzGetProjectRole_ProjectFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AuthzGetProjectRole_Project"},"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"}}]}}]} as unknown as DocumentNode; -export const AuthzGetServerRole_UserFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AuthzGetServerRole_User"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]} as unknown as DocumentNode; -export const AuthzGetWorkspace_WorkspaceFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AuthzGetWorkspace_Workspace"},"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":"slug"}}]}}]} as unknown as DocumentNode; -export const AuthzGetWorkspaceRole_WorkspaceFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AuthzGetWorkspaceRole_Workspace"},"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":"role"}}]}}]} as unknown as DocumentNode; -export const AuthzGetWorkspaceSsoProviderSession_WorkspaceFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AuthzGetWorkspaceSsoProviderSession_Workspace"},"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":"sso"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"provider"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"session"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"validUntil"}}]}}]}}]}}]} 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 SearchAutomateFunctionReleaseItemFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SearchAutomateFunctionReleaseItem"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateFunctionRelease"}},"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"}}]}}]} as unknown as DocumentNode; export const BillingActions_WorkspaceFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BillingActions_Workspace"},"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":"invitedTeam"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"invitesFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"plan"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}},{"kind":"Field","name":{"kind":"Name","value":"subscription"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"billingInterval"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"defaultRegion"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; export const HeaderWorkspaceSwitcherHeaderWorkspace_WorkspaceFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"HeaderWorkspaceSwitcherHeaderWorkspace_Workspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"InviteDialogWorkspace_Workspace"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"plan"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"InviteDialogWorkspace_Workspace"},"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":"domainBasedMembershipProtectionEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"domains"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"plan"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"subscription"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"seats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"guest"}},{"kind":"Field","name":{"kind":"Name","value":"plan"}}]}}]}}]}}]} as unknown as DocumentNode; @@ -7136,9 +7122,6 @@ export const AuthorizableAppMetadataDocument = {"kind":"Document","definitions": export const ActiveUserWorkspaceExistenceCheckDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ActiveUserWorkspaceExistenceCheck"},"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":"verified"}},{"kind":"Field","name":{"kind":"Name","value":"isOnboardingFinished"}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"0"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"0"}}],"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":"slug"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"discoverableWorkspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspaceJoinRequests"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"0"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]}}]} as unknown as DocumentNode; export const ActiveUserActiveWorkspaceCheckDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ActiveUserActiveWorkspaceCheck"},"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":"isProjectsActive"}},{"kind":"Field","name":{"kind":"Name","value":"activeWorkspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]} as unknown as DocumentNode; export const ProjectWorkspaceAccessCheckDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"projectWorkspaceAccessCheck"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","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":"role"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]}}]} as unknown as DocumentNode; -export const AuthzProjectMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AuthzProjectMetadata"},"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":"project"},"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":"FragmentSpread","name":{"kind":"Name","value":"AuthzGetProject_Project"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AuthzGetProjectRole_Project"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AuthzGetProject_Project"},"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":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AuthzGetProjectRole_Project"},"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"}}]}}]} as unknown as DocumentNode; -export const AuthzServerMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AuthzServerMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AuthzGetServerRole_User"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AuthzGetServerRole_User"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]} as unknown as DocumentNode; -export const AuthzWorkspaceMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AuthzWorkspaceMetadata"},"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":"FragmentSpread","name":{"kind":"Name","value":"AuthzGetWorkspace_Workspace"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AuthzGetWorkspaceRole_Workspace"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AuthzGetWorkspaceSsoProviderSession_Workspace"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AuthzGetWorkspace_Workspace"},"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":"slug"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AuthzGetWorkspaceRole_Workspace"},"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":"role"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AuthzGetWorkspaceSsoProviderSession_Workspace"},"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":"sso"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"provider"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"session"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"validUntil"}}]}}]}}]}}]} as unknown as DocumentNode; export const CreateAutomateFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateAutomateFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateAutomateFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"automateMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createFunction"},"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":"FragmentSpread","name":{"kind":"Name","value":"AutomationsFunctionsCard_AutomateFunction"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomateFunctionCreateDialogDoneStep_AutomateFunction"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomationsFunctionsCard_AutomateFunction"},"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":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"repo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"owner"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomateFunctionCreateDialogDoneStep_AutomateFunction"},"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":"repo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"owner"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomationsFunctionsCard_AutomateFunction"}}]}}]} as unknown as DocumentNode; export const UpdateAutomateFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateAutomateFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateAutomateFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"automateMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateFunction"},"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":"FragmentSpread","name":{"kind":"Name","value":"AutomateFunctionPage_AutomateFunction"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomateFunctionPageHeader_Function"},"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":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"repo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"owner"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"releases"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspaceIds"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomateFunctionPageParametersDialog_AutomateFunctionRelease"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateFunctionRelease"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inputSchema"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomateFunctionPageInfo_AutomateFunction"},"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":"repo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"owner"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"releases"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"1"}}],"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":"inputSchema"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"commitId"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomateFunctionPageParametersDialog_AutomateFunctionRelease"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomationsFunctionsCard_AutomateFunction"},"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":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"repo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"owner"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomateAutomationCreateDialogFunctionParametersStep_AutomateFunction"},"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":"releases"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"1"}}],"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":"inputSchema"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomateAutomationCreateDialog_AutomateFunction"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomationsFunctionsCard_AutomateFunction"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomateAutomationCreateDialogFunctionParametersStep_AutomateFunction"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomateFunctionPage_AutomateFunction"},"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":"description"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"supportedSourceApps"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomateFunctionPageHeader_Function"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomateFunctionPageInfo_AutomateFunction"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomateAutomationCreateDialog_AutomateFunction"}},{"kind":"Field","name":{"kind":"Name","value":"creator"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const SearchAutomateFunctionReleasesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SearchAutomateFunctionReleases"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"functionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"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":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateFunctionReleasesFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"automateFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"functionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"releases"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"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":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SearchAutomateFunctionReleaseItem"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SearchAutomateFunctionReleaseItem"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateFunctionRelease"}},"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"}}]}}]} as unknown as DocumentNode; @@ -7208,7 +7191,7 @@ export const CreateAutomationRevisionDocument = {"kind":"Document","definitions" export const TriggerAutomationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"TriggerAutomation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"automationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"automationMutations"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"projectId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"trigger"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"automationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"automationId"}}}]}]}}]}}]}}]} as unknown as DocumentNode; export const CreateTestAutomationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateTestAutomation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectTestAutomationCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"automationMutations"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"projectId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createTestAutomation"},"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":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageAutomationsRow_Automation"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FunctionRunStatusForSummary"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateFunctionRun"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomationRunDetails"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateRun"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"functionRuns"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FunctionRunStatusForSummary"}},{"kind":"Field","name":{"kind":"Name","value":"statusMessage"}}]}},{"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":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageAutomationsRow_Automation"},"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":"isTestAutomation"}},{"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":"model"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"runs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"10"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomationRunDetails"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}}]}}]}}]} as unknown as DocumentNode; export const MoveProjectToWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MoveProjectToWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"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":"moveToWorkspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}},{"kind":"Argument","name":{"kind":"Name","value":"projectId"},"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":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"projects"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectsMoveToWorkspaceDialog_Workspace"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"MoveProjectsDialog_Workspace"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceHasCustomDataResidency_Workspace"},"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":"defaultRegion"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectsWorkspaceSelect_Workspace"},"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":"role"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectsMoveToWorkspaceDialog_Workspace"},"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":"role"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkspaceHasCustomDataResidency_Workspace"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectsWorkspaceSelect_Workspace"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MoveProjectsDialog_Workspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectsMoveToWorkspaceDialog_Workspace"}},{"kind":"Field","name":{"kind":"Name","value":"projects"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","alias":{"kind":"Name","value":"modelCount"},"name":{"kind":"Name","value":"models"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"0"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"0"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const ProjectAccessCheckDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectAccessCheck"},"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":"project"},"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":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]} as unknown as DocumentNode; +export const ProjectAccessCheckDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectAccessCheck"},"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":"project"},"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":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canRead"},"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 ProjectRoleCheckDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectRoleCheck"},"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":"project"},"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"}}]}}]}}]} as unknown as DocumentNode; export const ProjectsDashboardQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectsDashboardQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UserProjectsFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"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":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"6"}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectsDashboard_UserProjectCollection"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectDashboardItem"}}]}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectsHiddenProjectWarning_User"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectsDashboardHeaderProjects_User"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectsPageTeamDialogManagePermissions_Project"},"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":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectsModelPageEmbed_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectsPageTeamDialogManagePermissions_Project"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageModelsActions_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectsModelPageEmbed_Project"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageModelsCardProject"},"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":"visibility"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageModelsActions_Project"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectDashboardItemNoModels"},"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":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"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":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageModelsCardProject"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PendingFileUpload"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FileUpload"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"modelName"}},{"kind":"Field","name":{"kind":"Name","value":"convertedStatus"}},{"kind":"Field","name":{"kind":"Name","value":"convertedMessage"}},{"kind":"Field","name":{"kind":"Name","value":"uploadDate"}},{"kind":"Field","name":{"kind":"Name","value":"convertedLastUpdate"}},{"kind":"Field","name":{"kind":"Name","value":"fileType"}},{"kind":"Field","name":{"kind":"Name","value":"fileName"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageModelsCardRenameDialog"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Model"}},"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":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageModelsCardDeleteDialog"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Model"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageModelsActions"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Model"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FunctionRunStatusForSummary"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateFunctionRun"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TriggeredAutomationsStatusSummary"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TriggeredAutomationsStatus"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"automationRuns"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"functionRuns"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"FunctionRunStatusForSummary"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomateRunsTriggerStatusDialogFunctionRun_AutomateFunctionRun"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateFunctionRun"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"results"}},{"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":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomationsStatusOrderedRuns_AutomationRun"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateRun"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","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":"functionRuns"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomateRunsTriggerStatusDialogRunsRows_AutomateRun"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateRun"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"functionRuns"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomateRunsTriggerStatusDialogFunctionRun_AutomateFunctionRun"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomationsStatusOrderedRuns_AutomationRun"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomateRunsTriggerStatusDialog_TriggeredAutomationsStatus"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TriggeredAutomationsStatus"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"automationRuns"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomateRunsTriggerStatusDialogRunsRows_AutomateRun"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomateRunsTriggerStatus_TriggeredAutomationsStatus"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TriggeredAutomationsStatus"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"TriggeredAutomationsStatusSummary"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomateRunsTriggerStatusDialog_TriggeredAutomationsStatus"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageLatestItemsModelItem"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Model"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","alias":{"kind":"Name","value":"versionCount"},"name":{"kind":"Name","value":"versions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"0"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"commentThreadCount"},"name":{"kind":"Name","value":"commentThreads"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"0"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pendingImportedVersions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PendingFileUpload"}}]}},{"kind":"Field","name":{"kind":"Name","value":"previewUrl"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageModelsCardRenameDialog"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageModelsCardDeleteDialog"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageModelsActions"}},{"kind":"Field","name":{"kind":"Name","value":"automationsStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomateRunsTriggerStatus_TriggeredAutomationsStatus"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedUserAvatar"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedUser"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectsInviteBanner"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingStreamCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"projectName"}},{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectsDashboard_UserProjectCollection"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"UserProjectCollection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"numberOfHidden"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectDashboardItem"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectDashboardItemNoModels"}},{"kind":"Field","name":{"kind":"Name","value":"models"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"4"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageLatestItemsModelItem"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pendingImportedModels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"4"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PendingFileUpload"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectsHiddenProjectWarning_User"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"expiredSsoSessions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectsDashboardHeaderProjects_User"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectInvites"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectsInviteBanner"}}]}}]}}]} as unknown as DocumentNode; export const ProjectsDashboardWorkspaceQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectsDashboardWorkspaceQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectsDashboardHeaderWorkspaces_User"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedUserAvatar"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedUser"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UseWorkspaceInviteManager_PendingWorkspaceCollaborator"},"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":"token"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceSlug"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceInviteBanner_PendingWorkspaceCollaborator"},"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":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceName"}},{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"UseWorkspaceInviteManager_PendingWorkspaceCollaborator"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectsDashboardHeaderWorkspaces_User"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceInvites"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkspaceInviteBanner_PendingWorkspaceCollaborator"}}]}}]}}]} as unknown as DocumentNode; @@ -7407,6 +7390,7 @@ export type AllObjectTypes = { PasswordStrengthCheckResults: PasswordStrengthCheckResults, PendingStreamCollaborator: PendingStreamCollaborator, PendingWorkspaceCollaborator: PendingWorkspaceCollaborator, + PermissionCheckResult: PermissionCheckResult, Price: Price, Project: Project, ProjectAccessRequest: ProjectAccessRequest, @@ -7423,6 +7407,7 @@ export type AllObjectTypes = { ProjectMutations: ProjectMutations, ProjectPendingModelsUpdatedMessage: ProjectPendingModelsUpdatedMessage, ProjectPendingVersionsUpdatedMessage: ProjectPendingVersionsUpdatedMessage, + ProjectPermissionChecks: ProjectPermissionChecks, ProjectRole: ProjectRole, ProjectTriggeredAutomationsStatusUpdatedMessage: ProjectTriggeredAutomationsStatusUpdatedMessage, ProjectUpdatedMessage: ProjectUpdatedMessage, @@ -7492,6 +7477,7 @@ export type AllObjectTypes = { WorkspaceJoinRequestMutations: WorkspaceJoinRequestMutations, WorkspaceMembersByRole: WorkspaceMembersByRole, WorkspaceMutations: WorkspaceMutations, + WorkspacePermissionChecks: WorkspacePermissionChecks, WorkspacePlan: WorkspacePlan, WorkspacePlanPrice: WorkspacePlanPrice, WorkspaceProjectMutations: WorkspaceProjectMutations, @@ -8062,6 +8048,12 @@ export type PendingWorkspaceCollaboratorFieldArgs = { workspaceName: {}, workspaceSlug: {}, } +export type PermissionCheckResultFieldArgs = { + authorized: {}, + code: {}, + message: {}, + payload: {}, +} export type PriceFieldArgs = { amount: {}, currency: {}, @@ -8089,6 +8081,7 @@ export type ProjectFieldArgs = { object: ProjectObjectArgs, pendingAccessRequests: {}, pendingImportedModels: ProjectPendingImportedModelsArgs, + permissions: {}, role: {}, sourceApps: {}, team: {}, @@ -8188,6 +8181,9 @@ export type ProjectPendingVersionsUpdatedMessageFieldArgs = { type: {}, version: {}, } +export type ProjectPermissionChecksFieldArgs = { + canRead: {}, +} export type ProjectRoleFieldArgs = { project: {}, role: {}, @@ -8666,6 +8662,7 @@ export type WorkspaceFieldArgs = { logo: {}, membersByRole: {}, name: {}, + permissions: {}, plan: {}, projects: WorkspaceProjectsArgs, readOnly: {}, @@ -8756,6 +8753,9 @@ export type WorkspaceMutationsFieldArgs = { updateRole: WorkspaceMutationsUpdateRoleArgs, updateSeatType: WorkspaceMutationsUpdateSeatTypeArgs, } +export type WorkspacePermissionChecksFieldArgs = { + canCreateProject: {}, +} export type WorkspacePlanFieldArgs = { createdAt: {}, name: {}, @@ -8887,6 +8887,7 @@ export type AllObjectFieldArgTypes = { PasswordStrengthCheckResults: PasswordStrengthCheckResultsFieldArgs, PendingStreamCollaborator: PendingStreamCollaboratorFieldArgs, PendingWorkspaceCollaborator: PendingWorkspaceCollaboratorFieldArgs, + PermissionCheckResult: PermissionCheckResultFieldArgs, Price: PriceFieldArgs, Project: ProjectFieldArgs, ProjectAccessRequest: ProjectAccessRequestFieldArgs, @@ -8903,6 +8904,7 @@ export type AllObjectFieldArgTypes = { ProjectMutations: ProjectMutationsFieldArgs, ProjectPendingModelsUpdatedMessage: ProjectPendingModelsUpdatedMessageFieldArgs, ProjectPendingVersionsUpdatedMessage: ProjectPendingVersionsUpdatedMessageFieldArgs, + ProjectPermissionChecks: ProjectPermissionChecksFieldArgs, ProjectRole: ProjectRoleFieldArgs, ProjectTriggeredAutomationsStatusUpdatedMessage: ProjectTriggeredAutomationsStatusUpdatedMessageFieldArgs, ProjectUpdatedMessage: ProjectUpdatedMessageFieldArgs, @@ -8972,6 +8974,7 @@ export type AllObjectFieldArgTypes = { WorkspaceJoinRequestMutations: WorkspaceJoinRequestMutationsFieldArgs, WorkspaceMembersByRole: WorkspaceMembersByRoleFieldArgs, WorkspaceMutations: WorkspaceMutationsFieldArgs, + WorkspacePermissionChecks: WorkspacePermissionChecksFieldArgs, WorkspacePlan: WorkspacePlanFieldArgs, WorkspacePlanPrice: WorkspacePlanPriceFieldArgs, WorkspaceProjectMutations: WorkspaceProjectMutationsFieldArgs, diff --git a/packages/frontend-2/lib/common/helpers/graphql.ts b/packages/frontend-2/lib/common/helpers/graphql.ts index 05737f348..650b26d4a 100644 --- a/packages/frontend-2/lib/common/helpers/graphql.ts +++ b/packages/frontend-2/lib/common/helpers/graphql.ts @@ -34,7 +34,8 @@ import { base64Encode } from '~/lib/common/helpers/encodeDecode' import type { ErrorResponse } from '@apollo/client/link/error' import type { AllObjectFieldArgTypes, - AllObjectTypes + AllObjectTypes, + PermissionCheckResult } from '~/lib/common/generated/gql/graphql' /** @@ -754,18 +755,60 @@ export const modifyObjectField = < export const hasErrorWith = (params: { errors: readonly GraphQLFormattedError[] | undefined - code?: string - codes?: string[] + codes?: Array message?: string }) => { - const { errors, code, message, codes } = params + const { errors, message, codes } = params if (!errors?.length) return undefined - if (!code?.length && !message?.length && !codes?.length) return undefined + if (!message?.length && !codes?.length) return undefined return errors.find((e) => { - const hasCode = code && e.extensions?.code === code const hasMessage = message && e.message.toLowerCase().includes(message) - const hasCodes = codes && codes.some((c) => e.extensions?.code === c) - return hasCode || hasMessage || hasCodes + const hasCodes = + codes && + codes.some((testCode) => { + const code = e.extensions?.code + if (!code || !isString(code)) return false + return isString(testCode) ? code === testCode : code.match(testCode) + }) + return hasMessage || hasCodes }) } + +export const errorsToAuthResult = (params: { + errors: readonly GraphQLFormattedError[] | undefined +}): PermissionCheckResult => { + const { errors } = params + if (!errors?.length) return { authorized: true, message: 'OK', code: 'OK' } + + // Prioritize common error codes + const commonAuthError = hasErrorWith({ + errors, + codes: [ + /forbidden/i, + /unauthorized/i, + /not[_\s]found/i, + /not[_\s]authorized/i, + 'SSO_SESSION_MISSING_OR_EXPIRED_ERROR' + ] + }) + if (commonAuthError) { + return { + authorized: false, + code: commonAuthError.extensions!.code as string, + message: commonAuthError.message, + payload: commonAuthError.extensions || null + } + } + + const firstError = errors[0] + return { + authorized: false, + code: + firstError.extensions?.code && isString(firstError.extensions?.code) + ? firstError.extensions?.code + : 'UNKNOWN_ERROR', + message: firstError.message, + payload: firstError.extensions || null + } +} diff --git a/packages/frontend-2/lib/projects/graphql/queries.ts b/packages/frontend-2/lib/projects/graphql/queries.ts index 054dcebd0..f2c00b904 100644 --- a/packages/frontend-2/lib/projects/graphql/queries.ts +++ b/packages/frontend-2/lib/projects/graphql/queries.ts @@ -4,10 +4,10 @@ export const projectAccessCheckQuery = graphql(` query ProjectAccessCheck($id: String!) { project(id: $id) { id - visibility - workspace { - id - slug + permissions { + canRead { + ...FullPermissionCheckResult + } } } } diff --git a/packages/frontend-2/middleware/requireValidProject.ts b/packages/frontend-2/middleware/requireValidProject.ts index b72da7ce9..a6d58bf87 100644 --- a/packages/frontend-2/middleware/requireValidProject.ts +++ b/packages/frontend-2/middleware/requireValidProject.ts @@ -1,53 +1,65 @@ -import { throwUncoveredError } from '@speckle/shared' -import { - ProjectNoAccessError, - ProjectNotFoundError, - ServerNoAccessError, - ServerNoSessionError, - WorkspaceNoAccessError, - WorkspaceSsoSessionNoAccessError -} from '@speckle/shared/authz' -import { useAuthPolicies } from '~/lib/auth/composables/authPolicies' -import { ActiveUserId } from '~/lib/auth/helpers/authPolicies' -import { loginRoute } from '~/lib/common/helpers/route' +import type { Optional } from '@speckle/shared' + +import { useApolloClientFromNuxt } from '~/lib/common/composables/graphql' +import { errorsToAuthResult } from '~/lib/common/helpers/graphql' +import { projectAccessCheckQuery } from '~/lib/projects/graphql/queries' +import { WorkspaceSsoErrorCodes } from '~/lib/workspaces/helpers/types' /** * Used in project page to validate that project ID refers to a valid project and redirects to 404 if not */ export default defineNuxtRouteMiddleware(async (to) => { const projectId = to.params.id as string - const authPolicies = useAuthPolicies() - const canAccess = await authPolicies.noCache().project.canQuery({ - projectId, - userId: ActiveUserId - }) - if (canAccess.isErr) { - switch (canAccess.error.code) { - case WorkspaceSsoSessionNoAccessError.code: { + const client = useApolloClientFromNuxt() + + const { data, errors } = await client + .query({ + query: projectAccessCheckQuery, + variables: { id: projectId }, + context: { + skipLoggingErrors: true + }, + fetchPolicy: 'network-only' + }) + .catch(convertThrowIntoFetchResult) + + // we may not even get to the authResult because of project() resolver errors, hence the mapping + // from errors to authResult + const authResult = data?.project.permissions.canRead || errorsToAuthResult({ errors }) + if (!authResult.authorized) { + switch (authResult.code) { + case WorkspaceSsoErrorCodes.SESSION_MISSING_OR_EXPIRED: { // Redirect to the SSO error page - const workspaceSlug = canAccess.error.payload.workspaceSlug - return navigateTo(`/workspaces/${workspaceSlug}/sso/session-error`) + const payload = authResult.payload as Optional<{ + workspaceSlug: string + }> + const workspaceSlug = payload?.workspaceSlug + if (workspaceSlug) { + return navigateTo(`/workspaces/${workspaceSlug}/sso/session-error`) + } } - case ProjectNotFoundError.code: { - return abortNavigation( - createError({ statusCode: 404, message: 'Project not found' }) - ) - } - case WorkspaceNoAccessError.code: - case ProjectNoAccessError.code: { + // eslint-disable-next-line no-fallthrough + case 'FORBIDDEN': return abortNavigation( createError({ statusCode: 403, - message: 'You do not have access to this project' + message: authResult.message + }) + ) + case 'STREAM_NOT_FOUND': + return abortNavigation( + createError({ + statusCode: 404, + message: authResult.message + }) + ) + default: + return abortNavigation( + createError({ + statusCode: 500, + message: authResult.message }) ) - } - case ServerNoAccessError.code: - case ServerNoSessionError.code: - return navigateTo(loginRoute) - default: { - throwUncoveredError(canAccess.error) - } } } }) diff --git a/packages/frontend-2/plugins/060-dataPreload.ts b/packages/frontend-2/plugins/060-dataPreload.ts index ef5b6fe9e..2cfdb2564 100644 --- a/packages/frontend-2/plugins/060-dataPreload.ts +++ b/packages/frontend-2/plugins/060-dataPreload.ts @@ -1,4 +1,3 @@ -import type { Optional } from '@speckle/shared' import { activeUserQuery } from '~/lib/auth/composables/activeUser' import { authLoginPanelQuery, @@ -6,8 +5,6 @@ import { } from '~/lib/auth/graphql/queries' import { usePreloadApolloQueries } from '~/lib/common/composables/graphql' import { mainServerInfoDataQuery } from '~/lib/core/composables/server' -import { projectAccessCheckQuery } from '~/lib/projects/graphql/queries' -import { workspaceAccessCheckQuery } from '~/lib/workspaces/graphql/queries' /** * Prefetches data for specific routes to avoid the problem of serial API requests @@ -24,9 +21,6 @@ export default defineNuxtPlugin(async (ctx) => { return } - const path = route.path - const idParam = route.params.id as Optional - const slugParam = route.params.slug as Optional const promises: Promise[] = [] // Standard/global @@ -36,36 +30,6 @@ export default defineNuxtPlugin(async (ctx) => { }) ) - // Preload project data - if (idParam && path.startsWith('/projects/')) { - promises.push( - preload({ - queries: [ - { - query: projectAccessCheckQuery, - variables: { id: idParam }, - context: { skipLoggingErrors: true } - } - ] - }) - ) - } - - // Preload workspace data - if (slugParam && path.startsWith('/workspaces/') && isWorkspacesEnabled.value) { - promises.push( - preload({ - queries: [ - { - query: workspaceAccessCheckQuery, - variables: { slug: slugParam }, - context: { skipLoggingErrors: true } - } - ] - }) - ) - } - // Preload viewer data if (route.meta.key === '/projects/:id/models/resources') { // Unable to preload this from vue components due to SSR being essentially turned off for the viewer diff --git a/packages/frontend-2/plugins/090-authPolicies.ts b/packages/frontend-2/plugins/090-authPolicies.ts deleted file mode 100644 index e1274fa23..000000000 --- a/packages/frontend-2/plugins/090-authPolicies.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { NuxtApp } from '#app' -import { Authz } from '@speckle/shared' -import { buildAuthPolicyLoaders } from '~/lib/auth/loaders/index' - -/** - * Set up @speckle/shared authPolicies & their loaders - */ -export default defineNuxtPlugin(async (nuxt) => { - const nuxtApp = nuxt as NuxtApp - const loaders = buildAuthPolicyLoaders({ nuxtApp }) - - return { - provide: { - authPolicies: { - ...Authz.authPoliciesFactory(loaders), - /** - * Skips Apollo Cache the first time a query is requested. Useful in middlewares - * where we want a fresh check to be invoked every time - */ - noCache: () => - Authz.authPoliciesFactory( - buildAuthPolicyLoaders({ - nuxtApp, - options: { noCache: true } - }) - ) - } - } - } -}) diff --git a/packages/server/assets/core/typedefs/common.graphql b/packages/server/assets/core/typedefs/common.graphql index 81c07cfab..4aaa23f40 100644 --- a/packages/server/assets/core/typedefs/common.graphql +++ b/packages/server/assets/core/typedefs/common.graphql @@ -39,3 +39,10 @@ type Price { currency: String! currencySymbol: String! } + +type PermissionCheckResult { + authorized: Boolean! + code: String! + message: String! + payload: JSONObject +} diff --git a/packages/server/assets/core/typedefs/permissions.graphql b/packages/server/assets/core/typedefs/permissions.graphql new file mode 100644 index 000000000..d76cd4e25 --- /dev/null +++ b/packages/server/assets/core/typedefs/permissions.graphql @@ -0,0 +1,7 @@ +extend type Project { + permissions: ProjectPermissionChecks! +} + +type ProjectPermissionChecks { + canRead: PermissionCheckResult! +} diff --git a/packages/server/assets/core/typedefs/projects.graphql b/packages/server/assets/core/typedefs/projects.graphql index dd009641c..810d5a955 100644 --- a/packages/server/assets/core/typedefs/projects.graphql +++ b/packages/server/assets/core/typedefs/projects.graphql @@ -207,6 +207,7 @@ input UserProjectsFilter { Only include projects where user has the specified roles """ onlyWithRoles: [String!] + workspaceId: ID } diff --git a/packages/server/assets/workspacesCore/typedefs/permissions.graphql b/packages/server/assets/workspacesCore/typedefs/permissions.graphql new file mode 100644 index 000000000..9ae487f24 --- /dev/null +++ b/packages/server/assets/workspacesCore/typedefs/permissions.graphql @@ -0,0 +1,7 @@ +extend type Workspace { + permissions: WorkspacePermissionChecks! +} + +type WorkspacePermissionChecks { + canCreateProject: PermissionCheckResult! +} diff --git a/packages/server/codegen.yml b/packages/server/codegen.yml index ec7a63d5e..a63608ed3 100644 --- a/packages/server/codegen.yml +++ b/packages/server/codegen.yml @@ -86,6 +86,8 @@ generates: ServerRegionItem: '@/modules/multiregion/helpers/graphTypes#ServerRegionItemGraphQLReturn' Price: '@/modules/gatekeeperCore/helpers/graphTypes#PriceGraphQLReturn' WorkspaceSubscription: '@/modules/gatekeeper/helpers/graphTypes#WorkspaceSubscriptionGraphQLReturn' + ProjectPermissionChecks: '@/modules/core/helpers/graphTypes#ProjectPermissionChecksGraphQLReturn' + WorkspacePermissionChecks: '@/modules/workspacesCore/helpers/graphTypes#WorkspacePermissionChecksGraphQLReturn' modules/cross-server-sync/graph/generated/graphql.ts: plugins: - 'typescript' diff --git a/packages/server/modules/core/authz/loaders/index.ts b/packages/server/modules/core/authz/loaders/index.ts index 9f92e4f65..57b3522ea 100644 --- a/packages/server/modules/core/authz/loaders/index.ts +++ b/packages/server/modules/core/authz/loaders/index.ts @@ -2,30 +2,22 @@ import { defineModuleLoaders } from '@/modules/loaders' import { getStreamFactory } from '@/modules/core/repositories/streams' import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' import { db } from '@/db/knex' -import { getUserServerRoleFactory } from '@/modules/shared/repositories/acl' -import { err, ok } from 'true-myth/result' -import { Authz } from '@speckle/shared' +// TODO: Move everything to use dataLoaders export default defineModuleLoaders(async () => { const getStream = getStreamFactory({ db }) - const getUserServerRole = getUserServerRoleFactory({ db }) return { getEnv: async () => getFeatureFlags(), - getProject: async ({ projectId }) => { - const project = await getStream({ streamId: projectId }) - if (!project) return err(new Authz.ProjectNotFoundError()) - return ok({ ...project, projectId: project.id }) + getProject: async ({ projectId }, { dataLoaders }) => { + return await dataLoaders.streams.getStream.load(projectId) }, getProjectRole: async ({ userId, projectId }) => { const project = await getStream({ streamId: projectId, userId }) - if (!project?.role) return err(new Authz.ProjectRoleNotFoundError()) - return ok(project.role) + return project?.role || null }, - getServerRole: async ({ userId }) => { - const role = await getUserServerRole({ userId }) - if (!role) return err(new Authz.ServerRoleNotFoundError()) - return ok(role) + getServerRole: async ({ userId }, { dataLoaders }) => { + return (await dataLoaders.users.getUser.load(userId))?.role || null } } }) diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index a906f00b8..7997ad9e6 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -1,11 +1,11 @@ import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql'; -import { StreamGraphQLReturn, CommitGraphQLReturn, ProjectGraphQLReturn, ObjectGraphQLReturn, VersionGraphQLReturn, ServerInviteGraphQLReturnType, ModelGraphQLReturn, ModelsTreeItemGraphQLReturn, MutationsObjectGraphQLReturn, LimitedUserGraphQLReturn, UserGraphQLReturn, GraphQLEmptyReturn, StreamCollaboratorGraphQLReturn, ProjectCollaboratorGraphQLReturn, ServerInfoGraphQLReturn, BranchGraphQLReturn } from '@/modules/core/helpers/graphTypes'; +import { StreamGraphQLReturn, CommitGraphQLReturn, ProjectGraphQLReturn, ObjectGraphQLReturn, VersionGraphQLReturn, ServerInviteGraphQLReturnType, ModelGraphQLReturn, ModelsTreeItemGraphQLReturn, MutationsObjectGraphQLReturn, LimitedUserGraphQLReturn, UserGraphQLReturn, GraphQLEmptyReturn, StreamCollaboratorGraphQLReturn, ProjectCollaboratorGraphQLReturn, ServerInfoGraphQLReturn, BranchGraphQLReturn, ProjectPermissionChecksGraphQLReturn } from '@/modules/core/helpers/graphTypes'; import { StreamAccessRequestGraphQLReturn, ProjectAccessRequestGraphQLReturn } from '@/modules/accessrequests/helpers/graphTypes'; import { CommentReplyAuthorCollectionGraphQLReturn, CommentGraphQLReturn } from '@/modules/comments/helpers/graphTypes'; import { PendingStreamCollaboratorGraphQLReturn } from '@/modules/serverinvites/helpers/graphTypes'; import { FileUploadGraphQLReturn } from '@/modules/fileuploads/helpers/types'; import { AutomateFunctionGraphQLReturn, AutomateFunctionReleaseGraphQLReturn, AutomationGraphQLReturn, AutomationRevisionGraphQLReturn, AutomationRevisionFunctionGraphQLReturn, AutomateRunGraphQLReturn, AutomationRunTriggerGraphQLReturn, AutomationRevisionTriggerDefinitionGraphQLReturn, AutomateFunctionRunGraphQLReturn, TriggeredAutomationsStatusGraphQLReturn, ProjectAutomationMutationsGraphQLReturn, ProjectTriggeredAutomationsStatusUpdatedMessageGraphQLReturn, ProjectAutomationsUpdatedMessageGraphQLReturn, UserAutomateInfoGraphQLReturn } from '@/modules/automate/helpers/graphTypes'; -import { WorkspaceGraphQLReturn, WorkspaceSsoGraphQLReturn, WorkspaceMutationsGraphQLReturn, WorkspaceJoinRequestMutationsGraphQLReturn, WorkspaceInviteMutationsGraphQLReturn, WorkspaceProjectMutationsGraphQLReturn, PendingWorkspaceCollaboratorGraphQLReturn, WorkspaceCollaboratorGraphQLReturn, WorkspaceJoinRequestGraphQLReturn, LimitedWorkspaceJoinRequestGraphQLReturn, ProjectRoleGraphQLReturn } from '@/modules/workspacesCore/helpers/graphTypes'; +import { WorkspaceGraphQLReturn, WorkspaceSsoGraphQLReturn, WorkspaceMutationsGraphQLReturn, WorkspaceJoinRequestMutationsGraphQLReturn, WorkspaceInviteMutationsGraphQLReturn, WorkspaceProjectMutationsGraphQLReturn, PendingWorkspaceCollaboratorGraphQLReturn, WorkspaceCollaboratorGraphQLReturn, WorkspaceJoinRequestGraphQLReturn, LimitedWorkspaceJoinRequestGraphQLReturn, ProjectRoleGraphQLReturn, WorkspacePermissionChecksGraphQLReturn } from '@/modules/workspacesCore/helpers/graphTypes'; import { WorkspaceBillingMutationsGraphQLReturn, WorkspaceSubscriptionGraphQLReturn } from '@/modules/gatekeeper/helpers/graphTypes'; import { WebhookGraphQLReturn } from '@/modules/webhooks/helpers/graphTypes'; import { SmartTextEditorValueGraphQLReturn } from '@/modules/core/services/richTextEditorService'; @@ -1984,6 +1984,14 @@ export type PendingWorkspaceCollaboratorsFilter = { search?: InputMaybe; }; +export type PermissionCheckResult = { + __typename?: 'PermissionCheckResult'; + authorized: Scalars['Boolean']['output']; + code: Scalars['String']['output']; + message: Scalars['String']['output']; + payload?: Maybe; +}; + export type Price = { __typename?: 'Price'; amount: Scalars['Float']['output']; @@ -2029,6 +2037,7 @@ export type Project = { pendingAccessRequests?: Maybe>; /** Returns a list models that are being created from a file import */ pendingImportedModels: Array; + permissions: ProjectPermissionChecks; /** Active user's role for this project. `null` if request is not authenticated, or the project is not explicitly shared with you. */ role?: Maybe; /** Source apps used in any models of this project */ @@ -2529,6 +2538,11 @@ export const ProjectPendingVersionsUpdatedMessageType = { } as const; export type ProjectPendingVersionsUpdatedMessageType = typeof ProjectPendingVersionsUpdatedMessageType[keyof typeof ProjectPendingVersionsUpdatedMessageType]; +export type ProjectPermissionChecks = { + __typename?: 'ProjectPermissionChecks'; + canRead: PermissionCheckResult; +}; + export type ProjectRole = { __typename?: 'ProjectRole'; project: Project; @@ -4359,6 +4373,7 @@ export type Workspace = { logo?: Maybe; membersByRole?: Maybe; name: Scalars['String']['output']; + permissions: WorkspacePermissionChecks; plan?: Maybe; projects: ProjectCollection; /** A Workspace is marked as readOnly if its trial period is finished or a paid plan is subscribed but payment has failed */ @@ -4723,6 +4738,11 @@ export const WorkspacePaymentMethod = { } as const; export type WorkspacePaymentMethod = typeof WorkspacePaymentMethod[keyof typeof WorkspacePaymentMethod]; +export type WorkspacePermissionChecks = { + __typename?: 'WorkspacePermissionChecks'; + canCreateProject: PermissionCheckResult; +}; + export type WorkspacePlan = { __typename?: 'WorkspacePlan'; createdAt: Scalars['DateTime']['output']; @@ -5173,6 +5193,7 @@ export type ResolversTypes = { PendingStreamCollaborator: ResolverTypeWrapper; PendingWorkspaceCollaborator: ResolverTypeWrapper; PendingWorkspaceCollaboratorsFilter: PendingWorkspaceCollaboratorsFilter; + PermissionCheckResult: ResolverTypeWrapper; Price: ResolverTypeWrapper; Project: ResolverTypeWrapper; ProjectAccessRequest: ResolverTypeWrapper; @@ -5204,6 +5225,7 @@ export type ResolversTypes = { ProjectPendingModelsUpdatedMessageType: ProjectPendingModelsUpdatedMessageType; ProjectPendingVersionsUpdatedMessage: ResolverTypeWrapper & { version: ResolversTypes['FileUpload'] }>; ProjectPendingVersionsUpdatedMessageType: ProjectPendingVersionsUpdatedMessageType; + ProjectPermissionChecks: ResolverTypeWrapper; ProjectRole: ResolverTypeWrapper; ProjectTestAutomationCreateInput: ProjectTestAutomationCreateInput; ProjectTriggeredAutomationsStatusUpdatedMessage: ResolverTypeWrapper; @@ -5330,6 +5352,7 @@ export type ResolversTypes = { WorkspaceMembersByRole: ResolverTypeWrapper; WorkspaceMutations: ResolverTypeWrapper; WorkspacePaymentMethod: WorkspacePaymentMethod; + WorkspacePermissionChecks: ResolverTypeWrapper; WorkspacePlan: ResolverTypeWrapper; WorkspacePlanPrice: ResolverTypeWrapper & { monthly?: Maybe, yearly?: Maybe }>; WorkspacePlanStatuses: WorkspacePlanStatuses; @@ -5491,6 +5514,7 @@ export type ResolversParentTypes = { PendingStreamCollaborator: PendingStreamCollaboratorGraphQLReturn; PendingWorkspaceCollaborator: PendingWorkspaceCollaboratorGraphQLReturn; PendingWorkspaceCollaboratorsFilter: PendingWorkspaceCollaboratorsFilter; + PermissionCheckResult: PermissionCheckResult; Price: PriceGraphQLReturn; Project: ProjectGraphQLReturn; ProjectAccessRequest: ProjectAccessRequestGraphQLReturn; @@ -5516,6 +5540,7 @@ export type ResolversParentTypes = { ProjectMutations: MutationsObjectGraphQLReturn; ProjectPendingModelsUpdatedMessage: Omit & { model: ResolversParentTypes['FileUpload'] }; ProjectPendingVersionsUpdatedMessage: Omit & { version: ResolversParentTypes['FileUpload'] }; + ProjectPermissionChecks: ProjectPermissionChecksGraphQLReturn; ProjectRole: ProjectRoleGraphQLReturn; ProjectTestAutomationCreateInput: ProjectTestAutomationCreateInput; ProjectTriggeredAutomationsStatusUpdatedMessage: ProjectTriggeredAutomationsStatusUpdatedMessageGraphQLReturn; @@ -5626,6 +5651,7 @@ export type ResolversParentTypes = { WorkspaceJoinRequestMutations: WorkspaceJoinRequestMutationsGraphQLReturn; WorkspaceMembersByRole: WorkspaceMembersByRole; WorkspaceMutations: WorkspaceMutationsGraphQLReturn; + WorkspacePermissionChecks: WorkspacePermissionChecksGraphQLReturn; WorkspacePlan: WorkspacePlan; WorkspacePlanPrice: Omit & { monthly?: Maybe, yearly?: Maybe }; WorkspaceProjectCreateInput: WorkspaceProjectCreateInput; @@ -6389,6 +6415,14 @@ export type PendingWorkspaceCollaboratorResolvers; }; +export type PermissionCheckResultResolvers = { + authorized?: Resolver; + code?: Resolver; + message?: Resolver; + payload?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type PriceResolvers = { amount?: Resolver; currency?: Resolver; @@ -6418,6 +6452,7 @@ export type ProjectResolvers, ParentType, ContextType, RequireFields>; pendingAccessRequests?: Resolver>, ParentType, ContextType>; pendingImportedModels?: Resolver, ParentType, ContextType, RequireFields>; + permissions?: Resolver; role?: Resolver, ParentType, ContextType>; sourceApps?: Resolver, ParentType, ContextType>; team?: Resolver, ParentType, ContextType>; @@ -6547,6 +6582,11 @@ export type ProjectPendingVersionsUpdatedMessageResolvers; }; +export type ProjectPermissionChecksResolvers = { + canRead?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type ProjectRoleResolvers = { project?: Resolver; role?: Resolver; @@ -7135,6 +7175,7 @@ export type WorkspaceResolvers, ParentType, ContextType>; membersByRole?: Resolver, ParentType, ContextType>; name?: Resolver; + permissions?: Resolver; plan?: Resolver, ParentType, ContextType>; projects?: Resolver>; readOnly?: Resolver; @@ -7251,6 +7292,11 @@ export type WorkspaceMutationsResolvers; }; +export type WorkspacePermissionChecksResolvers = { + canCreateProject?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type WorkspacePlanResolvers = { createdAt?: Resolver; name?: Resolver; @@ -7413,6 +7459,7 @@ export type Resolvers = { PasswordStrengthCheckResults?: PasswordStrengthCheckResultsResolvers; PendingStreamCollaborator?: PendingStreamCollaboratorResolvers; PendingWorkspaceCollaborator?: PendingWorkspaceCollaboratorResolvers; + PermissionCheckResult?: PermissionCheckResultResolvers; Price?: PriceResolvers; Project?: ProjectResolvers; ProjectAccessRequest?: ProjectAccessRequestResolvers; @@ -7429,6 +7476,7 @@ export type Resolvers = { ProjectMutations?: ProjectMutationsResolvers; ProjectPendingModelsUpdatedMessage?: ProjectPendingModelsUpdatedMessageResolvers; ProjectPendingVersionsUpdatedMessage?: ProjectPendingVersionsUpdatedMessageResolvers; + ProjectPermissionChecks?: ProjectPermissionChecksResolvers; ProjectRole?: ProjectRoleResolvers; ProjectTriggeredAutomationsStatusUpdatedMessage?: ProjectTriggeredAutomationsStatusUpdatedMessageResolvers; ProjectUpdatedMessage?: ProjectUpdatedMessageResolvers; @@ -7498,6 +7546,7 @@ export type Resolvers = { WorkspaceJoinRequestMutations?: WorkspaceJoinRequestMutationsResolvers; WorkspaceMembersByRole?: WorkspaceMembersByRoleResolvers; WorkspaceMutations?: WorkspaceMutationsResolvers; + WorkspacePermissionChecks?: WorkspacePermissionChecksResolvers; WorkspacePlan?: WorkspacePlanResolvers; WorkspacePlanPrice?: WorkspacePlanPriceResolvers; WorkspaceProjectMutations?: WorkspaceProjectMutationsResolvers; diff --git a/packages/server/modules/core/graph/resolvers/permissions.ts b/packages/server/modules/core/graph/resolvers/permissions.ts new file mode 100644 index 000000000..759ed5fb5 --- /dev/null +++ b/packages/server/modules/core/graph/resolvers/permissions.ts @@ -0,0 +1,17 @@ +import { Resolvers } from '@/modules/core/graph/generated/graphql' +import { Authz } from '@speckle/shared' + +export default { + Project: { + permissions: (parent) => ({ projectId: parent.id }) + }, + ProjectPermissionChecks: { + canRead: async (parent, _args, ctx) => { + const canRead = await ctx.authPolicies.project.canRead({ + projectId: parent.projectId, + userId: ctx.userId + }) + return Authz.toGraphqlResult(canRead) + } + } +} as Resolvers diff --git a/packages/server/modules/core/graph/resolvers/projects.ts b/packages/server/modules/core/graph/resolvers/projects.ts index fb8b54c95..7e8fe7506 100644 --- a/packages/server/modules/core/graph/resolvers/projects.ts +++ b/packages/server/modules/core/graph/resolvers/projects.ts @@ -91,6 +91,7 @@ import { has } from 'lodash' import { throwUncoveredError } from '@speckle/shared' import { ForbiddenError } from '@/modules/shared/errors' import { Authz } from '@speckle/shared' +import { SsoSessionMissingOrExpiredError } from '@/modules/workspacesCore/errors' const getServerInfo = getServerInfoFactory({ db }) const getUsers = getUsersFactory({ db }) @@ -179,7 +180,7 @@ const getUserStreamsCount = getUserStreamsCountFactory({ db }) export = { Query: { async project(_parent, args, context) { - const canQuery = await context.authPolicies.project.canQuery({ + const canQuery = await context.authPolicies.project.canRead({ projectId: args.id, userId: context.userId }) @@ -187,11 +188,16 @@ export = { if (!canQuery.isOk) { switch (canQuery.error.code) { case Authz.ProjectNotFoundError.code: - throw new StreamNotFoundError() + throw new StreamNotFoundError('Project not found') case Authz.ProjectNoAccessError.code: case Authz.WorkspaceNoAccessError.code: - case Authz.WorkspaceSsoSessionNoAccessError.code: throw new ForbiddenError(canQuery.error.message) + case Authz.WorkspaceSsoSessionNoAccessError.code: + throw new SsoSessionMissingOrExpiredError(canQuery.error.message, { + info: { + workspaceSlug: canQuery.error.payload.workspaceSlug + } + }) case Authz.ServerNoAccessError.code: case Authz.ServerNoSessionError.code: throw new ForbiddenError(canQuery.error.message) diff --git a/packages/server/modules/core/helpers/graphTypes.ts b/packages/server/modules/core/helpers/graphTypes.ts index a07703cf0..bfefa4c3c 100644 --- a/packages/server/modules/core/helpers/graphTypes.ts +++ b/packages/server/modules/core/helpers/graphTypes.ts @@ -136,3 +136,7 @@ export type ProjectCollaboratorGraphQLReturn = { role: StreamRoles projectId: string } + +export type ProjectPermissionChecksGraphQLReturn = { + projectId: string +} diff --git a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts index 454e8d2f6..f34f753f0 100644 --- a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts +++ b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts @@ -1964,6 +1964,14 @@ export type PendingWorkspaceCollaboratorsFilter = { search?: InputMaybe; }; +export type PermissionCheckResult = { + __typename?: 'PermissionCheckResult'; + authorized: Scalars['Boolean']['output']; + code: Scalars['String']['output']; + message: Scalars['String']['output']; + payload?: Maybe; +}; + export type Price = { __typename?: 'Price'; amount: Scalars['Float']['output']; @@ -2009,6 +2017,7 @@ export type Project = { pendingAccessRequests?: Maybe>; /** Returns a list models that are being created from a file import */ pendingImportedModels: Array; + permissions: ProjectPermissionChecks; /** Active user's role for this project. `null` if request is not authenticated, or the project is not explicitly shared with you. */ role?: Maybe; /** Source apps used in any models of this project */ @@ -2509,6 +2518,11 @@ export const ProjectPendingVersionsUpdatedMessageType = { } as const; export type ProjectPendingVersionsUpdatedMessageType = typeof ProjectPendingVersionsUpdatedMessageType[keyof typeof ProjectPendingVersionsUpdatedMessageType]; +export type ProjectPermissionChecks = { + __typename?: 'ProjectPermissionChecks'; + canRead: PermissionCheckResult; +}; + export type ProjectRole = { __typename?: 'ProjectRole'; project: Project; @@ -4339,6 +4353,7 @@ export type Workspace = { logo?: Maybe; membersByRole?: Maybe; name: Scalars['String']['output']; + permissions: WorkspacePermissionChecks; plan?: Maybe; projects: ProjectCollection; /** A Workspace is marked as readOnly if its trial period is finished or a paid plan is subscribed but payment has failed */ @@ -4703,6 +4718,11 @@ export const WorkspacePaymentMethod = { } as const; export type WorkspacePaymentMethod = typeof WorkspacePaymentMethod[keyof typeof WorkspacePaymentMethod]; +export type WorkspacePermissionChecks = { + __typename?: 'WorkspacePermissionChecks'; + canCreateProject: PermissionCheckResult; +}; + export type WorkspacePlan = { __typename?: 'WorkspacePlan'; createdAt: Scalars['DateTime']['output']; diff --git a/packages/server/modules/gatekeeper/domain/billing.ts b/packages/server/modules/gatekeeper/domain/billing.ts index 5e3b95c08..209a80d38 100644 --- a/packages/server/modules/gatekeeper/domain/billing.ts +++ b/packages/server/modules/gatekeeper/domain/billing.ts @@ -1,8 +1,4 @@ import { - PaidWorkspacePlan, - TrialWorkspacePlan, - UnpaidWorkspacePlan, - WorkspacePlan, WorkspacePlanProductPrices, WorkspacePricingProducts } from '@/modules/gatekeeperCore/domain/billing' @@ -10,7 +6,11 @@ import { Workspace, WorkspaceAcl } from '@/modules/workspacesCore/domain/types' import { Nullable, Optional, + PaidWorkspacePlan, PaidWorkspacePlans, + TrialWorkspacePlan, + UnpaidWorkspacePlan, + WorkspacePlan, WorkspacePlanBillingIntervals } from '@speckle/shared' import { OverrideProperties } from 'type-fest' diff --git a/packages/server/modules/gatekeeper/domain/operations.ts b/packages/server/modules/gatekeeper/domain/operations.ts index 902d34f70..8a300cf03 100644 --- a/packages/server/modules/gatekeeper/domain/operations.ts +++ b/packages/server/modules/gatekeeper/domain/operations.ts @@ -1,8 +1,8 @@ import { WorkspaceSeat } from '@/modules/gatekeeper/domain/billing' -import { WorkspacePlan } from '@/modules/gatekeeperCore/domain/billing' import { Workspace } from '@/modules/workspacesCore/domain/types' import { Optional, + WorkspacePlan, WorkspacePlanFeatures, WorkspacePlans, WorkspacePlanStatuses diff --git a/packages/server/modules/gatekeeper/events/eventListener.ts b/packages/server/modules/gatekeeper/events/eventListener.ts index 4a1fd847d..8582be2a2 100644 --- a/packages/server/modules/gatekeeper/events/eventListener.ts +++ b/packages/server/modules/gatekeeper/events/eventListener.ts @@ -1,9 +1,7 @@ import { reconcileWorkspaceSubscriptionFactory } from '@/modules/gatekeeper/clients/stripe' import { getWorkspacePlanFactory, - getWorkspaceSubscriptionFactory, - upsertTrialWorkspacePlanFactory, - upsertUnpaidWorkspacePlanFactory + getWorkspaceSubscriptionFactory } from '@/modules/gatekeeper/repositories/billing' import { countSeatsByTypeInWorkspaceFactory } from '@/modules/gatekeeper/repositories/workspaceSeat' import { @@ -14,15 +12,12 @@ import { getWorkspacePlanPriceId, getWorkspacePlanProductId } from '@/modules/gatekeeper/stripe' -import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' import { getEventBus } from '@/modules/shared/services/eventBus' import { countWorkspaceRoleWithOptionalProjectRoleFactory } from '@/modules/workspaces/repositories/workspaces' import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events' import { Knex } from 'knex' import Stripe from 'stripe' -const { FF_GATEKEEPER_FORCE_FREE_PLAN } = getFeatureFlags() - export const initializeEventListenersFactory = ({ db, stripe }: { db: Knex; stripe: Stripe }) => () => { @@ -64,28 +59,6 @@ export const initializeEventListenersFactory = ...payload.seat, seatType: payload.seat.type }) - }), - eventBus.listen(WorkspaceEvents.Created, async ({ payload }) => { - // TODO: based on a feature flag, we can force new workspaces into the free plan here - if (FF_GATEKEEPER_FORCE_FREE_PLAN) { - await upsertUnpaidWorkspacePlanFactory({ db })({ - workspacePlan: { - name: 'free', - status: 'valid', - workspaceId: payload.workspace.id, - createdAt: new Date() - } - }) - } else { - await upsertTrialWorkspacePlanFactory({ db })({ - workspacePlan: { - name: 'starter', - status: 'trial', - workspaceId: payload.workspace.id, - createdAt: new Date() - } - }) - } }) ] diff --git a/packages/server/modules/gatekeeper/repositories/billing.ts b/packages/server/modules/gatekeeper/repositories/billing.ts index 4e5e749b4..7ce5e4b0a 100644 --- a/packages/server/modules/gatekeeper/repositories/billing.ts +++ b/packages/server/modules/gatekeeper/repositories/billing.ts @@ -23,11 +23,14 @@ import { GetWorkspacesByPlanDaysTillExpiry, GetWorkspacePlanByProjectId } from '@/modules/gatekeeper/domain/operations' -import { WorkspacePlan } from '@/modules/gatekeeperCore/domain/billing' import { formatJsonArrayRecords } from '@/modules/shared/helpers/dbHelper' import { Workspace } from '@/modules/workspacesCore/domain/types' import { Workspaces } from '@/modules/workspacesCore/helpers/db' -import { PaidWorkspacePlansNew, PaidWorkspacePlansOld } from '@speckle/shared' +import { + PaidWorkspacePlansNew, + PaidWorkspacePlansOld, + WorkspacePlan +} from '@speckle/shared' import { Knex } from 'knex' import { omit } from 'lodash' diff --git a/packages/server/modules/gatekeeper/services/readOnly.ts b/packages/server/modules/gatekeeper/services/readOnly.ts index 827f8c12d..0741dedac 100644 --- a/packages/server/modules/gatekeeper/services/readOnly.ts +++ b/packages/server/modules/gatekeeper/services/readOnly.ts @@ -1,23 +1,7 @@ import { GetWorkspacePlan } from '@/modules/gatekeeper/domain/billing' import { GetWorkspacePlanByProjectId } from '@/modules/gatekeeper/domain/operations' -import { WorkspacePlan } from '@/modules/gatekeeperCore/domain/billing' import { Workspace } from '@/modules/workspacesCore/domain/types' -import { throwUncoveredError } from '@speckle/shared' - -const isWorkspacePlanStatusReadOnly = (status: WorkspacePlan['status']) => { - switch (status) { - case 'cancelationScheduled': - case 'valid': - case 'trial': - case 'paymentFailed': - return false - case 'expired': - case 'canceled': - return true - default: - throwUncoveredError(status) - } -} +import { isWorkspacePlanStatusReadOnly } from '@speckle/shared' export const isWorkspaceReadOnlyFactory = ({ getWorkspacePlan }: { getWorkspacePlan: GetWorkspacePlan }) => diff --git a/packages/server/modules/gatekeeper/services/subscriptions.ts b/packages/server/modules/gatekeeper/services/subscriptions.ts index fecd34783..58b4a6b3e 100644 --- a/packages/server/modules/gatekeeper/services/subscriptions.ts +++ b/packages/server/modules/gatekeeper/services/subscriptions.ts @@ -22,11 +22,11 @@ import { CountWorkspaceRoleWithOptionalProjectRole } from '@/modules/workspaces/ import { PaidWorkspacePlanStatuses, throwUncoveredError, + WorkspacePlan, WorkspaceRoles } from '@speckle/shared' import { cloneDeep, sum } from 'lodash' import { CountSeatsByTypeInWorkspace } from '@/modules/gatekeeper/domain/operations' -import { WorkspacePlan } from '@/modules/gatekeeperCore/domain/billing' export const handleSubscriptionUpdateFactory = ({ diff --git a/packages/server/modules/gatekeeper/services/workspacePlans.ts b/packages/server/modules/gatekeeper/services/workspacePlans.ts index 263bfa794..0155972ac 100644 --- a/packages/server/modules/gatekeeper/services/workspacePlans.ts +++ b/packages/server/modules/gatekeeper/services/workspacePlans.ts @@ -1,10 +1,9 @@ import { UpsertWorkspacePlan } from '@/modules/gatekeeper/domain/billing' import { InvalidWorkspacePlanStatus } from '@/modules/gatekeeper/errors/billing' -import { WorkspacePlan } from '@/modules/gatekeeperCore/domain/billing' import { EventBusEmit } from '@/modules/shared/services/eventBus' import { GetWorkspace } from '@/modules/workspaces/domain/operations' import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace' -import { throwUncoveredError } from '@speckle/shared' +import { throwUncoveredError, WorkspacePlan } from '@speckle/shared' export const updateWorkspacePlanFactory = ({ diff --git a/packages/server/modules/gatekeeper/tests/unit/checkout.spec.ts b/packages/server/modules/gatekeeper/tests/unit/checkout.spec.ts index d08988ea2..4ec702a38 100644 --- a/packages/server/modules/gatekeeper/tests/unit/checkout.spec.ts +++ b/packages/server/modules/gatekeeper/tests/unit/checkout.spec.ts @@ -14,8 +14,11 @@ import { WorkspaceSubscription } from '@/modules/gatekeeper/domain/billing' import { omit } from 'lodash' -import { PaidWorkspacePlan } from '@/modules/gatekeeperCore/domain/billing' -import { PaidWorkspacePlans, WorkspacePlanBillingIntervals } from '@speckle/shared' +import { + PaidWorkspacePlan, + PaidWorkspacePlans, + WorkspacePlanBillingIntervals +} from '@speckle/shared' import { startCheckoutSessionFactoryNew as startCheckoutSessionFactory, startCheckoutSessionFactoryOld diff --git a/packages/server/modules/gatekeeper/tests/unit/featureAuthorization.spec.ts b/packages/server/modules/gatekeeper/tests/unit/featureAuthorization.spec.ts index b32a3c597..84fe61c60 100644 --- a/packages/server/modules/gatekeeper/tests/unit/featureAuthorization.spec.ts +++ b/packages/server/modules/gatekeeper/tests/unit/featureAuthorization.spec.ts @@ -1,5 +1,5 @@ import { canWorkspaceAccessFeatureFactory } from '@/modules/gatekeeper/services/featureAuthorization' -import { WorkspacePlan } from '@/modules/gatekeeperCore/domain/billing' +import { WorkspacePlan } from '@speckle/shared' import { expect } from 'chai' import cryptoRandomString from 'crypto-random-string' diff --git a/packages/server/modules/gatekeeper/tests/unit/subscriptions.spec.ts b/packages/server/modules/gatekeeper/tests/unit/subscriptions.spec.ts index 50f287a58..48f4bd32d 100644 --- a/packages/server/modules/gatekeeper/tests/unit/subscriptions.spec.ts +++ b/packages/server/modules/gatekeeper/tests/unit/subscriptions.spec.ts @@ -28,9 +28,8 @@ import { createTestSubscriptionData, createTestWorkspaceSubscription } from '@/modules/gatekeeper/tests/helpers' -import { WorkspacePlan } from '@/modules/gatekeeperCore/domain/billing' import { expectToThrow } from '@/test/assertionHelper' -import { throwUncoveredError } from '@speckle/shared' +import { throwUncoveredError, WorkspacePlan } from '@speckle/shared' import { expect } from 'chai' import cryptoRandomString from 'crypto-random-string' import { omit } from 'lodash' diff --git a/packages/server/modules/gatekeeper/tests/unit/workspacePlans.spec.ts b/packages/server/modules/gatekeeper/tests/unit/workspacePlans.spec.ts index 97dd5f897..da69e8531 100644 --- a/packages/server/modules/gatekeeper/tests/unit/workspacePlans.spec.ts +++ b/packages/server/modules/gatekeeper/tests/unit/workspacePlans.spec.ts @@ -1,10 +1,10 @@ import { InvalidWorkspacePlanStatus } from '@/modules/gatekeeper/errors/billing' import { updateWorkspacePlanFactory } from '@/modules/gatekeeper/services/workspacePlans' -import { WorkspacePlan } from '@/modules/gatekeeperCore/domain/billing' import { EventBusEmit } from '@/modules/shared/services/eventBus' import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace' import { WorkspaceWithOptionalRole } from '@/modules/workspacesCore/domain/types' import { expectToThrow } from '@/test/assertionHelper' +import { WorkspacePlan } from '@speckle/shared' import { expect } from 'chai' import cryptoRandomString from 'crypto-random-string' import { omit } from 'lodash' diff --git a/packages/server/modules/gatekeeperCore/domain/billing.ts b/packages/server/modules/gatekeeperCore/domain/billing.ts index 402fe3c9b..8ba41e93c 100644 --- a/packages/server/modules/gatekeeperCore/domain/billing.ts +++ b/packages/server/modules/gatekeeperCore/domain/billing.ts @@ -1,10 +1,5 @@ import { PaidWorkspacePlans, - PaidWorkspacePlanStatuses, - TrialEnabledPaidWorkspacePlans, - TrialWorkspacePlanStatuses, - UnpaidWorkspacePlans, - UnpaidWorkspacePlanStatuses, WorkspacePlanBillingIntervals, WorkspacePlans } from '@speckle/shared' @@ -15,27 +10,6 @@ import { OverrideProperties, SetOptional } from 'type-fest' */ export type WorkspacePricingProducts = PaidWorkspacePlans | 'guest' -type BaseWorkspacePlan = { - workspaceId: string - createdAt: Date -} - -export type PaidWorkspacePlan = BaseWorkspacePlan & { - name: PaidWorkspacePlans - status: PaidWorkspacePlanStatuses -} - -export type TrialWorkspacePlan = BaseWorkspacePlan & { - name: TrialEnabledPaidWorkspacePlans - status: TrialWorkspacePlanStatuses -} - -export type UnpaidWorkspacePlan = BaseWorkspacePlan & { - name: UnpaidWorkspacePlans - status: UnpaidWorkspacePlanStatuses -} -export type WorkspacePlan = PaidWorkspacePlan | TrialWorkspacePlan | UnpaidWorkspacePlan - type WorkspacePlanProductsMetadata = OverrideProperties< Record< WorkspacePricingProducts, diff --git a/packages/server/modules/gatekeeperCore/domain/events.ts b/packages/server/modules/gatekeeperCore/domain/events.ts index cdc1a8d49..0d7efc569 100644 --- a/packages/server/modules/gatekeeperCore/domain/events.ts +++ b/packages/server/modules/gatekeeperCore/domain/events.ts @@ -1,4 +1,4 @@ -import { WorkspacePlan } from '@/modules/gatekeeperCore/domain/billing' +import { WorkspacePlan } from '@speckle/shared' export const gatekeeperEventNamespace = 'gatekeeper' as const diff --git a/packages/server/modules/index.ts b/packages/server/modules/index.ts index 89388d42b..19c0b96c3 100644 --- a/packages/server/modules/index.ts +++ b/packages/server/modules/index.ts @@ -4,7 +4,7 @@ import fs from 'fs' import path from 'path' import { appRoot, packageRoot } from '@/bootstrap' -import { values, merge, camelCase, reduce, intersection, difference } from 'lodash' +import { values, merge, camelCase, reduce, intersection, difference, set } from 'lodash' import baseTypeDefs from '@/modules/core/graph/schema/baseTypeDefs' import { scalarResolvers } from '@/modules/core/graph/scalars' import { makeExecutableSchema } from '@graphql-tools/schema' @@ -24,12 +24,17 @@ import { AppMocksConfig } from '@/modules/mocks' import { SpeckleModuleMocksConfig } from '@/modules/shared/helpers/mocks' import { LoaderConfigurationError, LogicError } from '@/modules/shared/errors' import type { Registry } from 'prom-client' -import type { defineModuleLoaders } from '@/modules/loaders' +import type { + defineModuleLoaders, + ServerLoaders, + ServerLoadersContext +} from '@/modules/loaders' import { inMemoryCacheProviderFactory, wrapWithCache } from '@/modules/shared/utils/caching' import TTLCache from '@isaacs/ttlcache' +import { buildRequestLoaders, RequestDataLoaders } from '@/modules/core/loaders' /** * Cached speckle module requires @@ -134,7 +139,9 @@ export const init = async (params: { app: Express; metricsRegister: Registry }) } // Validate & cache authz loaders - await moduleAuthLoaders() + await moduleAuthLoaders({ + dataLoaders: undefined + }) hasInitializationOccurred = true } @@ -316,10 +323,16 @@ export const moduleMockConfigs = ( return mockConfigs } -export const moduleAuthLoaders = async () => { +export const moduleAuthLoaders = async (params: { + dataLoaders?: RequestDataLoaders +}) => { const enabledModuleNames = getEnabledModuleNames() let loaders: Partial = {} + const dataLoaders = params.dataLoaders || (await buildRequestLoaders({ auth: false })) + const ctx: ServerLoadersContext = { + dataLoaders + } // load auth loaders from /modules and in same order as the whitelist const codeModuleDirs = fs.readdirSync(`${appRoot}/modules`) @@ -334,9 +347,29 @@ export const moduleAuthLoaders = async () => { .map((l) => l.default) .filter(isNonNullable)[0] as Optional> + // Load the actual loaders + const newLoaders = await moduleLoadersBuilderFn?.() + const newServerLoaders: Partial = Object.entries( + newLoaders || {} + ).reduce((acc, entry) => { + const key = entry[0] as Authz.AuthCheckContextLoaderKeys + const loader = entry[1] as Required[typeof key] + + // Feed in ctx to all loader functions + const wrappedLoader = (...args: any[]) => { + const newArgs = [...args, ctx] + return loader(...newArgs) + } + + // Using set because of TS typing difficulty + set(acc, key, wrappedLoader) + + return acc + }, {} as Partial) + loaders = { ...loaders, - ...(await moduleLoadersBuilderFn?.()) + ...newServerLoaders } } @@ -381,6 +414,9 @@ export const moduleAuthLoaders = async () => { return { loaders: loadersWithCache, - clearCache: () => cache.clear() + clearCache: () => { + cache.clear() + dataLoaders.clearAll() + } } } diff --git a/packages/server/modules/loaders.ts b/packages/server/modules/loaders.ts index a4705670d..3f04dd7bc 100644 --- a/packages/server/modules/loaders.ts +++ b/packages/server/modules/loaders.ts @@ -1,8 +1,20 @@ +import { RequestDataLoaders } from '@/modules/core/loaders' import { Authz, MaybeAsync } from '@speckle/shared' +export type ServerLoadersContext = { + dataLoaders: RequestDataLoaders +} + +// Inject extra argument to all loaders, e.g. for GQL dataloaders +export type ServerLoaders = Partial<{ + [K in keyof Authz.AuthCheckContextLoaders]: Authz.AuthCheckContextLoaders[K] extends ( + ...args: infer A + ) => infer R + ? (...args: [...A, ctx: ServerLoadersContext]) => R + : never +}> + // define being an arg simplifes usage in export default calls -export const defineModuleLoaders = ( - define: () => MaybeAsync> -) => { +export const defineModuleLoaders = (define: () => MaybeAsync) => { return async () => await define() } diff --git a/packages/server/modules/shared/middleware/index.ts b/packages/server/modules/shared/middleware/index.ts index 1bd635649..4c34bfca0 100644 --- a/packages/server/modules/shared/middleware/index.ts +++ b/packages/server/modules/shared/middleware/index.ts @@ -209,10 +209,8 @@ export async function buildContext(params?: { await wait(delay) } - const [authLoaders, dataLoaders] = await Promise.all([ - moduleAuthLoaders(), - buildRequestLoaders(ctx, { cleanLoadersEarly }) - ]) + const dataLoaders = await buildRequestLoaders(ctx, { cleanLoadersEarly }) + const authLoaders = await moduleAuthLoaders({ dataLoaders }) const authPolicies = Authz.authPoliciesFactory(authLoaders.loaders) return { diff --git a/packages/server/modules/workspaces/authz/loaders/index.ts b/packages/server/modules/workspaces/authz/loaders/index.ts index 14c9fed81..0ba8065fb 100644 --- a/packages/server/modules/workspaces/authz/loaders/index.ts +++ b/packages/server/modules/workspaces/authz/loaders/index.ts @@ -1,46 +1,62 @@ import { db } from '@/db/knex' +import { getWorkspacePlanFactory } from '@/modules/gatekeeper/repositories/billing' import { defineModuleLoaders } from '@/modules/loaders' import { getUserSsoSessionFactory, getWorkspaceSsoProviderRecordFactory } from '@/modules/workspaces/repositories/sso' -import { - getWorkspaceFactory, - getWorkspaceRoleForUserFactory -} from '@/modules/workspaces/repositories/workspaces' -import { Authz } from '@speckle/shared' -import { err, ok } from 'true-myth/result' +import { getWorkspaceRoleForUserFactory } from '@/modules/workspaces/repositories/workspaces' +import { WorkspacePaidPlanConfigs, WorkspaceUnpaidPlanConfigs } from '@speckle/shared' +// TODO: Move everything to use dataLoaders export default defineModuleLoaders(async () => { - const getWorkspace = getWorkspaceFactory({ db }) + const getWorkspacePlan = getWorkspacePlanFactory({ db }) + return { - getWorkspace: async ({ workspaceId }) => { - const workspace = await getWorkspace({ workspaceId }) - if (!workspace) return err(new Authz.WorkspaceNotFoundError()) - return ok(workspace) + getWorkspace: async ({ workspaceId }, { dataLoaders }) => { + return (await dataLoaders.workspaces!.getWorkspace.load(workspaceId)) || null }, getWorkspaceRole: async ({ userId, workspaceId }) => { const role = await getWorkspaceRoleForUserFactory({ db })({ userId, workspaceId }) - if (!role) return err(new Authz.WorkspaceRoleNotFoundError()) - return ok(role.role) + return role?.role || null }, getWorkspaceSsoSession: async ({ userId, workspaceId }) => { const ssoSession = await getUserSsoSessionFactory({ db })({ userId, workspaceId }) - if (!ssoSession) return err(new Authz.WorkspaceSsoSessionNotFoundError()) - return ok(ssoSession) + return ssoSession || null }, getWorkspaceSsoProvider: async ({ workspaceId }) => { const ssoProvider = await getWorkspaceSsoProviderRecordFactory({ db })({ workspaceId }) - if (!ssoProvider) return err(new Authz.WorkspaceSsoProviderNotFoundError()) - return ok(ssoProvider) + return ssoProvider || null + }, + getWorkspaceSeat: async ({ userId, workspaceId }, { dataLoaders }) => { + return ( + ( + await dataLoaders.gatekeeper!.getUserWorkspaceSeat.load({ + userId, + workspaceId + }) + )?.type || null + ) + }, + getWorkspaceProjectCount: async ({ workspaceId }, { dataLoaders }) => { + return await dataLoaders.workspaces!.getProjectCount.load(workspaceId) + }, + getWorkspacePlan: async ({ workspaceId }) => { + return await getWorkspacePlan({ workspaceId }) + }, + getWorkspaceLimits: async ({ workspaceId }) => { + const plan = await getWorkspacePlan({ workspaceId }) + if (!plan) return null + const config = { ...WorkspacePaidPlanConfigs, ...WorkspaceUnpaidPlanConfigs } + return config[plan.name]?.limits ?? null } } }) diff --git a/packages/server/modules/workspaces/domain/operations.ts b/packages/server/modules/workspaces/domain/operations.ts index 8912a8b04..cc07c2f28 100644 --- a/packages/server/modules/workspaces/domain/operations.ts +++ b/packages/server/modules/workspaces/domain/operations.ts @@ -234,6 +234,12 @@ export type QueryAllWorkspaceProjects = ( args: QueryAllWorkspaceProjectsArgs ) => AsyncGenerator +export type GetWorkspacesProjectsCounts = (params: { + workspaceIds: string[] +}) => Promise<{ + [workspaceId: string]: number +}> + /** Workspace Project Roles */ type GrantWorkspaceProjectRolesArgs = { diff --git a/packages/server/modules/workspaces/errors/sso.ts b/packages/server/modules/workspaces/errors/sso.ts index 7c9a05823..636d12151 100644 --- a/packages/server/modules/workspaces/errors/sso.ts +++ b/packages/server/modules/workspaces/errors/sso.ts @@ -1,13 +1,7 @@ import { UserEmail } from '@/modules/core/domain/userEmails/types' import { User } from '@/modules/core/domain/users/types' import { BaseError } from '@/modules/shared/errors/base' - -export class SsoSessionMissingOrExpiredError extends BaseError { - static defaultMessage = - 'No valid SSO session found for the given workspace. Please sign in.' - static code = 'SSO_SESSION_MISSING_OR_EXPIRED_ERROR' - static statusCode = 401 -} +export { SsoSessionMissingOrExpiredError } from '@/modules/workspacesCore/errors' export class SsoVerificationCodeMissingError extends BaseError { static defaultMessage = 'Cannot find verification token. Restart authentication flow.' diff --git a/packages/server/modules/workspaces/events/eventListener.ts b/packages/server/modules/workspaces/events/eventListener.ts index 9a1aca87a..5c6d640e7 100644 --- a/packages/server/modules/workspaces/events/eventListener.ts +++ b/packages/server/modules/workspaces/events/eventListener.ts @@ -100,7 +100,9 @@ import { getDefaultRegionFactory } from '@/modules/workspaces/repositories/regio import { getWorkspacePlanFactory, getWorkspaceSubscriptionFactory, - getWorkspaceWithPlanFactory + getWorkspaceWithPlanFactory, + upsertTrialWorkspacePlanFactory, + upsertUnpaidWorkspacePlanFactory } from '@/modules/gatekeeper/repositories/billing' import { ensureValidWorkspaceRoleSeatFactory } from '@/modules/workspaces/services/workspaceSeat' import { @@ -119,6 +121,9 @@ import { import { getUserFactory } from '@/modules/core/repositories/users' import { authorizeResolver } from '@/modules/shared' import { isNewPaidPlanType, isNewPlanType } from '@/modules/gatekeeper/helpers/plans' +import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' + +const { FF_GATEKEEPER_FORCE_FREE_PLAN } = getFeatureFlags() export const onProjectCreatedFactory = (deps: { @@ -235,7 +240,11 @@ export const onWorkspaceAuthorizedFactory = const session = await getUserSsoSession({ userId, workspaceId }) if (!session || !isValidSsoSession(session)) { const workspace = await getWorkspace({ workspaceId }) - throw new SsoSessionMissingOrExpiredError(workspace?.slug) + throw new SsoSessionMissingOrExpiredError(workspace?.slug, { + info: { + workspaceSlug: workspace?.slug + } + }) } } @@ -732,6 +741,28 @@ export const initializeEventListenersFactory = }) await onWorkspaceAuthorized(payload) }), + eventBus.listen(WorkspaceEvents.Created, async ({ payload }) => { + // TODO: based on a feature flag, we can force new workspaces into the free plan here + if (FF_GATEKEEPER_FORCE_FREE_PLAN) { + await upsertUnpaidWorkspacePlanFactory({ db })({ + workspacePlan: { + name: 'free', + status: 'valid', + workspaceId: payload.workspace.id, + createdAt: new Date() + } + }) + } else { + await upsertTrialWorkspacePlanFactory({ db })({ + workspacePlan: { + name: 'starter', + status: 'trial', + workspaceId: payload.workspace.id, + createdAt: new Date() + } + }) + } + }), eventBus.listen(WorkspaceEvents.RoleDeleted, async ({ payload }) => { const trx = await db.transaction() const onWorkspaceRoleDeleted = onWorkspaceRoleDeletedFactory({ diff --git a/packages/server/modules/workspaces/graph/dataloaders/workspaces.ts b/packages/server/modules/workspaces/graph/dataloaders/workspaces.ts index 052de75f8..a2fea08d5 100644 --- a/packages/server/modules/workspaces/graph/dataloaders/workspaces.ts +++ b/packages/server/modules/workspaces/graph/dataloaders/workspaces.ts @@ -2,7 +2,8 @@ import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' import { defineRequestDataloaders } from '@/modules/shared/helpers/graphqlHelper' import { getWorkspaceDomainsFactory, - getWorkspacesFactory + getWorkspacesFactory, + getWorkspacesProjectsCountsFactory } from '@/modules/workspaces/repositories/workspaces' import { WorkspaceDomain, @@ -21,6 +22,7 @@ const dataLoadersDefinition = defineRequestDataloaders( ({ ctx, createLoader, deps: { db } }) => { const getWorkspaces = getWorkspacesFactory({ db }) const getWorkspaceDomains = getWorkspaceDomainsFactory({ db }) + const getWorkspacesProjectsCounts = getWorkspacesProjectsCountsFactory({ db }) return { workspaces: { @@ -35,7 +37,16 @@ const dataLoadersDefinition = defineRequestDataloaders( ) return ids.map((id) => results[id] || null) } - ) + ), + /** + * Get workspace project count + */ + getProjectCount: createLoader(async (ids) => { + const results = await getWorkspacesProjectsCounts({ + workspaceIds: ids.slice() + }) + return ids.map((id) => results[id] ?? null) + }) }, workspaceDomains: { /** diff --git a/packages/server/modules/workspaces/graph/resolvers/permissions.ts b/packages/server/modules/workspaces/graph/resolvers/permissions.ts new file mode 100644 index 000000000..024ff241b --- /dev/null +++ b/packages/server/modules/workspaces/graph/resolvers/permissions.ts @@ -0,0 +1,19 @@ +import { Resolvers } from '@/modules/core/graph/generated/graphql' +import { Authz } from '@speckle/shared' + +export default { + Workspace: { + permissions: (parent) => ({ + workspaceId: parent.id + }) + }, + WorkspacePermissionChecks: { + canCreateProject: async (parent, _args, ctx) => { + const canCreateProject = await ctx.authPolicies.workspace.canCreateProject({ + workspaceId: parent.workspaceId, + userId: ctx.userId + }) + return Authz.toGraphqlResult(canCreateProject) + } + } +} as Resolvers diff --git a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts index c4ceb5ff0..37c60b726 100644 --- a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts +++ b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts @@ -111,6 +111,7 @@ import { getWorkspacesForUserFactory } from '@/modules/workspaces/services/retrieval' import { + Authz, Roles, WorkspaceRoles, removeNullOrUndefinedKeys, @@ -183,7 +184,7 @@ import { } from '@/modules/gatekeeper/repositories/billing' import { Knex } from 'knex' import { getPaginatedItemsFactory } from '@/modules/shared/services/paginatedItems' -import { BadRequestError } from '@/modules/shared/errors' +import { BadRequestError, ForbiddenError } from '@/modules/shared/errors' import { dismissWorkspaceJoinRequestFactory, requestToJoinWorkspaceFactory @@ -958,13 +959,27 @@ export = FF_WORKSPACES_MODULE_ENABLED throw new RateLimitError(rateLimitResult) } - await authorizeResolver( - context.userId!, - args.input.workspaceId, - Roles.Workspace.Member, - context.resourceAccessRules, - OperationTypeNode.MUTATION - ) + const canCreate = await context.authPolicies.workspace.canCreateProject({ + userId: context.userId, + workspaceId: args.input.workspaceId + }) + + if (!canCreate.isOk) { + switch (canCreate.error.code) { + case Authz.WorkspacesNotEnabledError.code: + case Authz.WorkspaceNoAccessError.code: + case Authz.WorkspaceReadOnlyError.code: + case Authz.WorkspaceNoEditorSeatError.code: + case Authz.WorkspaceNotEnoughPermissionsError.code: + case Authz.WorkspaceSsoSessionNoAccessError.code: + case Authz.WorkspaceLimitsReachedError.code: + case Authz.ServerNoSessionError.code: + case Authz.ServerNoAccessError.code: + throw new ForbiddenError(canCreate.error.message) + default: + throwUncoveredError(canCreate.error) + } + } const createWorkspaceProject = createWorkspaceProjectFactory({ getDefaultRegion: getDefaultRegionFactory({ db }) diff --git a/packages/server/modules/workspaces/repositories/workspaces.ts b/packages/server/modules/workspaces/repositories/workspaces.ts index c407d11ba..aeec114fd 100644 --- a/packages/server/modules/workspaces/repositories/workspaces.ts +++ b/packages/server/modules/workspaces/repositories/workspaces.ts @@ -26,6 +26,7 @@ import { GetWorkspaceRolesForUser, GetWorkspaceWithDomains, GetWorkspaces, + GetWorkspacesProjectsCounts, QueryWorkspaces, StoreWorkspaceDomain, UpsertWorkspace, @@ -508,3 +509,31 @@ export const upsertWorkspaceCreationStateFactory = .onConflict('workspaceId') .merge() } + +export const getWorkspacesProjectsCountsFactory = + (deps: { db: Knex }): GetWorkspacesProjectsCounts => + async (params) => { + const ret = params.workspaceIds.reduce((acc, workspaceId) => { + acc[workspaceId] = 0 + return acc + }, {} as Record) + + const q = tables + .streams(deps.db) + .select< + { + workspaceId: string + count: string + }[] + >([Streams.col.workspaceId, knex.raw('count(*) as count')]) + .whereIn(Streams.col.workspaceId, params.workspaceIds) + .groupBy(Streams.col.workspaceId) + + const res = await q + + for (const { workspaceId, count } of res) { + ret[workspaceId] = parseInt(count) + } + + return ret + } diff --git a/packages/server/modules/workspaces/tests/helpers/creation.ts b/packages/server/modules/workspaces/tests/helpers/creation.ts index 27af4a05c..1a546d0a1 100644 --- a/packages/server/modules/workspaces/tests/helpers/creation.ts +++ b/packages/server/modules/workspaces/tests/helpers/creation.ts @@ -38,7 +38,12 @@ import { import { BasicTestUser } from '@/test/authHelper' import { CreateWorkspaceInviteMutationVariables } from '@/test/graphql/generated/graphql' import cryptoRandomString from 'crypto-random-string' -import { MaybeNullOrUndefined, Roles, WorkspaceRoles } from '@speckle/shared' +import { + MaybeNullOrUndefined, + Roles, + WorkspacePlan, + WorkspaceRoles +} from '@speckle/shared' import { getStreamFactory } from '@/modules/core/repositories/streams' import { getUserFactory } from '@/modules/core/repositories/users' import { getServerInfoFactory } from '@/modules/core/repositories/server' @@ -70,7 +75,6 @@ import { upsertRegionAssignmentFactory } from '@/modules/workspaces/repositories/regions' import { getDb } from '@/modules/multiregion/utils/dbSelector' -import { WorkspacePlan } from '@/modules/gatekeeperCore/domain/billing' import { WorkspaceSeatType } from '@/modules/gatekeeper/domain/billing' import { assignWorkspaceSeatFactory, diff --git a/packages/server/modules/workspacesCore/authz/loaders/index.ts b/packages/server/modules/workspacesCore/authz/loaders/index.ts index 81af81928..30f9a85e1 100644 --- a/packages/server/modules/workspacesCore/authz/loaders/index.ts +++ b/packages/server/modules/workspacesCore/authz/loaders/index.ts @@ -13,5 +13,17 @@ export default defineModuleLoaders(() => ({ }, getWorkspaceSsoProvider: async () => { throw new LoaderUnsupportedError() + }, + getWorkspaceSeat: async () => { + throw new LoaderUnsupportedError() + }, + getWorkspaceProjectCount: async () => { + throw new LoaderUnsupportedError() + }, + getWorkspacePlan: async () => { + throw new LoaderUnsupportedError() + }, + getWorkspaceLimits: async () => { + throw new LoaderUnsupportedError() } })) diff --git a/packages/server/modules/workspacesCore/errors/index.ts b/packages/server/modules/workspacesCore/errors/index.ts new file mode 100644 index 000000000..222dd9c34 --- /dev/null +++ b/packages/server/modules/workspacesCore/errors/index.ts @@ -0,0 +1,10 @@ +import { BaseError } from '@/modules/shared/errors/base' + +export class SsoSessionMissingOrExpiredError extends BaseError<{ + workspaceSlug: string +}> { + static defaultMessage = + 'No valid SSO session found for the given workspace. Please sign in.' + static code = 'SSO_SESSION_MISSING_OR_EXPIRED_ERROR' + static statusCode = 401 +} diff --git a/packages/server/modules/workspacesCore/helpers/graphTypes.ts b/packages/server/modules/workspacesCore/helpers/graphTypes.ts index 2b5347b69..46d94773e 100644 --- a/packages/server/modules/workspacesCore/helpers/graphTypes.ts +++ b/packages/server/modules/workspacesCore/helpers/graphTypes.ts @@ -36,3 +36,7 @@ export type PendingWorkspaceCollaboratorGraphQLReturn = { } export type WorkspaceCollaboratorGraphQLReturn = WorkspaceTeamMember + +export type WorkspacePermissionChecksGraphQLReturn = { + workspaceId: string +} diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index 15bbb6bc7..306ab63b7 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -1965,6 +1965,14 @@ export type PendingWorkspaceCollaboratorsFilter = { search?: InputMaybe; }; +export type PermissionCheckResult = { + __typename?: 'PermissionCheckResult'; + authorized: Scalars['Boolean']['output']; + code: Scalars['String']['output']; + message: Scalars['String']['output']; + payload?: Maybe; +}; + export type Price = { __typename?: 'Price'; amount: Scalars['Float']['output']; @@ -2010,6 +2018,7 @@ export type Project = { pendingAccessRequests?: Maybe>; /** Returns a list models that are being created from a file import */ pendingImportedModels: Array; + permissions: ProjectPermissionChecks; /** Active user's role for this project. `null` if request is not authenticated, or the project is not explicitly shared with you. */ role?: Maybe; /** Source apps used in any models of this project */ @@ -2510,6 +2519,11 @@ export const ProjectPendingVersionsUpdatedMessageType = { } as const; export type ProjectPendingVersionsUpdatedMessageType = typeof ProjectPendingVersionsUpdatedMessageType[keyof typeof ProjectPendingVersionsUpdatedMessageType]; +export type ProjectPermissionChecks = { + __typename?: 'ProjectPermissionChecks'; + canRead: PermissionCheckResult; +}; + export type ProjectRole = { __typename?: 'ProjectRole'; project: Project; @@ -4340,6 +4354,7 @@ export type Workspace = { logo?: Maybe; membersByRole?: Maybe; name: Scalars['String']['output']; + permissions: WorkspacePermissionChecks; plan?: Maybe; projects: ProjectCollection; /** A Workspace is marked as readOnly if its trial period is finished or a paid plan is subscribed but payment has failed */ @@ -4704,6 +4719,11 @@ export const WorkspacePaymentMethod = { } as const; export type WorkspacePaymentMethod = typeof WorkspacePaymentMethod[keyof typeof WorkspacePaymentMethod]; +export type WorkspacePermissionChecks = { + __typename?: 'WorkspacePermissionChecks'; + canCreateProject: PermissionCheckResult; +}; + export type WorkspacePlan = { __typename?: 'WorkspacePlan'; createdAt: Scalars['DateTime']['output']; diff --git a/packages/shared/src/authz/checks/projects.spec.ts b/packages/shared/src/authz/checks/projects.spec.ts index 24bd344ef..4e4e56dab 100644 --- a/packages/shared/src/authz/checks/projects.spec.ts +++ b/packages/shared/src/authz/checks/projects.spec.ts @@ -2,33 +2,13 @@ import { describe, expect, it } from 'vitest' import { isPubliclyReadableProject, hasMinimumProjectRole } from './projects.js' import cryptoRandomString from 'crypto-random-string' import { Roles } from '../../core/index.js' -import { err, ok } from 'true-myth/result' -import { - ProjectNoAccessError, - ProjectNotFoundError, - ProjectRoleNotFoundError, - WorkspaceSsoSessionNoAccessError -} from '../domain/authErrors.js' import { getProjectFake } from '../../tests/fakes.js' describe('project checks', () => { describe('isPubliclyReadableProject returns a function, that', () => { - it('throws uncoveredError for unexpected loader errors', async () => { - await expect( - isPubliclyReadableProject({ - // @ts-expect-error deliberately testing an unexpeceted error type - getProject: async () => err(new ProjectRoleNotFoundError()) - })({ projectId: cryptoRandomString({ length: 10 }) }) - ).rejects.toThrowError(/Uncovered error/) - }) - it.each([ - ProjectNotFoundError, - ProjectNoAccessError, - WorkspaceSsoSessionNoAccessError - ])('turns expected loader error $code into false ', async (loaderError) => { + it('turns not found project into false ', async () => { const result = await isPubliclyReadableProject({ - getProject: async () => - err(new loaderError({ payload: { workspaceSlug: 'foo' } })) + getProject: async () => null })({ projectId: cryptoRandomString({ length: 10 }) }) expect(result).toEqual(false) }) @@ -46,31 +26,20 @@ describe('project checks', () => { }) }) describe('hasMinimumProjectRole returns a function, that', () => { - it('throws uncoveredError for unexpected loader errors', async () => { + it('returns false for not existing project roles', async () => { await expect( hasMinimumProjectRole({ - // @ts-expect-error deliberately testing an unexpeceted error type - getProjectRole: async () => err(new ProjectNotFoundError()) + getProjectRole: async () => null })({ projectId: cryptoRandomString({ length: 10 }), userId: cryptoRandomString({ length: 10 }), role: Roles.Stream.Contributor }) - ).rejects.toThrowError(/Uncovered error/) - }) - it('returns false, if there is no role for the user', async () => { - const result = await hasMinimumProjectRole({ - getProjectRole: () => Promise.resolve(err(new ProjectRoleNotFoundError())) - })({ - projectId: cryptoRandomString({ length: 10 }), - userId: cryptoRandomString({ length: 10 }), - role: Roles.Stream.Contributor - }) - expect(result).toEqual(false) + ).resolves.toStrictEqual(false) }) it('returns false, if the role is not sufficient', async () => { const result = await hasMinimumProjectRole({ - getProjectRole: () => Promise.resolve(ok(Roles.Stream.Reviewer)) + getProjectRole: async () => Roles.Stream.Reviewer })({ projectId: cryptoRandomString({ length: 10 }), userId: cryptoRandomString({ length: 10 }), @@ -80,7 +49,7 @@ describe('project checks', () => { }) it('returns true, if the role is sufficient', async () => { const result = await hasMinimumProjectRole({ - getProjectRole: () => Promise.resolve(ok(Roles.Stream.Contributor)) + getProjectRole: async () => Roles.Stream.Contributor })({ projectId: cryptoRandomString({ length: 10 }), userId: cryptoRandomString({ length: 10 }), diff --git a/packages/shared/src/authz/checks/projects.ts b/packages/shared/src/authz/checks/projects.ts index d6b8af83e..4708fc97f 100644 --- a/packages/shared/src/authz/checks/projects.ts +++ b/packages/shared/src/authz/checks/projects.ts @@ -1,6 +1,7 @@ -import { StreamRoles, throwUncoveredError } from '../../core/index.js' -import { AuthPolicyCheck, ProjectContext, UserContext } from '../domain/policies.js' +import { StreamRoles } from '../../core/index.js' +import { AuthPolicyCheck } from '../domain/policies.js' import { isMinimumProjectRole } from '../domain/logic/roles.js' +import { ProjectContext, UserContext } from '../domain/context.js' export const hasMinimumProjectRole: AuthPolicyCheck< 'getProjectRole', @@ -9,32 +10,14 @@ export const hasMinimumProjectRole: AuthPolicyCheck< (loaders) => async ({ userId, projectId, role: requiredProjectRole }) => { const userProjectRole = await loaders.getProjectRole({ userId, projectId }) - if (userProjectRole.isErr) { - switch (userProjectRole.error.code) { - case 'ProjectRoleNotFound': - return false - default: - throwUncoveredError(userProjectRole.error.code) - } - } - return isMinimumProjectRole(userProjectRole.value, requiredProjectRole) + if (!userProjectRole) return false + return isMinimumProjectRole(userProjectRole, requiredProjectRole) } export const isPubliclyReadableProject: AuthPolicyCheck<'getProject', ProjectContext> = (loaders) => async ({ projectId }) => { const project = await loaders.getProject({ projectId }) - if (project.isErr) { - switch (project.error.code) { - case 'ProjectNotFound': - return false - case 'ProjectNoAccess': - return false - case 'WorkspaceSsoSessionNoAccess': - return false - default: - throwUncoveredError(project.error) - } - } - return project.value.isPublic || project.value.isDiscoverable + if (!project) return false + return project.isPublic || project.isDiscoverable } diff --git a/packages/shared/src/authz/checks/serverRole.spec.ts b/packages/shared/src/authz/checks/serverRole.spec.ts index d7cccb542..3b3043029 100644 --- a/packages/shared/src/authz/checks/serverRole.spec.ts +++ b/packages/shared/src/authz/checks/serverRole.spec.ts @@ -1,34 +1,18 @@ import { describe, expect, it } from 'vitest' import { hasMinimumServerRole, canUseAdminOverride } from './serverRole.js' import cryptoRandomString from 'crypto-random-string' -import { err, ok } from 'true-myth/result' -import { - ServerRoleNotFoundError, - ProjectRoleNotFoundError -} from '../domain/authErrors.js' import { parseFeatureFlags } from '../../environment/index.js' describe('hasMinimumServerRole returns a function, that', () => { - it('throws uncoveredError for unexpected loader errors', async () => { - await expect( - hasMinimumServerRole({ - // @ts-expect-error deliberately testing an unexpected loader error - getServerRole: async () => err(new ProjectRoleNotFoundError()) - })({ userId: cryptoRandomString({ length: 10 }), role: 'server:user' }) - ).rejects.toThrowError(/Uncovered error/) + it('turns non existing server roles into false ', async () => { + const result = await hasMinimumServerRole({ + getServerRole: async () => null + })({ userId: cryptoRandomString({ length: 10 }), role: 'server:user' }) + expect(result).toEqual(false) }) - it.each([ServerRoleNotFoundError])( - 'turns expected loader error $code into false ', - async (loaderError) => { - const result = await hasMinimumServerRole({ - getServerRole: async () => err(new loaderError()) - })({ userId: cryptoRandomString({ length: 10 }), role: 'server:user' }) - expect(result).toEqual(false) - } - ) it('returns false for smaller roles', async () => { const result = await hasMinimumServerRole({ - getServerRole: async () => Promise.resolve(ok('server:user')) + getServerRole: async () => 'server:user' })({ userId: cryptoRandomString({ length: 9 }), role: 'server:admin' @@ -37,7 +21,7 @@ describe('hasMinimumServerRole returns a function, that', () => { }) it('returns true for roles with enough power', async () => { const result = await hasMinimumServerRole({ - getServerRole: () => Promise.resolve(ok('server:admin')) + getServerRole: async () => 'server:admin' })({ userId: cryptoRandomString({ length: 9 }), role: 'server:guest' @@ -59,21 +43,21 @@ describe('canUseAdminOverride returns a function, that', () => { it('returns false for non admins if admin override is not enabled', async () => { const result = await canUseAdminOverride({ getEnv: async () => parseFeatureFlags({}), - getServerRole: async () => ok('server:user') + getServerRole: async () => 'server:user' })({ userId: cryptoRandomString({ length: 10 }) }) expect(result).toEqual(false) }) it('returns false for non admins if admin override is enabled', async () => { const result = await canUseAdminOverride({ getEnv: async () => parseFeatureFlags({ FF_ADMIN_OVERRIDE_ENABLED: 'true' }), - getServerRole: async () => ok('server:user') + getServerRole: async () => 'server:user' })({ userId: cryptoRandomString({ length: 10 }) }) expect(result).toEqual(false) }) it('returns true for admins if admin override is enabled', async () => { const result = await canUseAdminOverride({ getEnv: async () => parseFeatureFlags({ FF_ADMIN_OVERRIDE_ENABLED: 'true' }), - getServerRole: async () => ok('server:admin') + getServerRole: async () => 'server:admin' })({ userId: cryptoRandomString({ length: 10 }) }) expect(result).toEqual(true) }) diff --git a/packages/shared/src/authz/checks/serverRole.ts b/packages/shared/src/authz/checks/serverRole.ts index 969ac07b5..c3a9c0748 100644 --- a/packages/shared/src/authz/checks/serverRole.ts +++ b/packages/shared/src/authz/checks/serverRole.ts @@ -1,29 +1,22 @@ import { Roles, ServerRoles } from '../../core/constants.js' -import { throwUncoveredError } from '../../core/index.js' +import { UserContext } from '../domain/context.js' import { isMinimumServerRole } from '../domain/logic/roles.js' import { AuthPolicyCheck } from '../domain/policies.js' export const hasMinimumServerRole: AuthPolicyCheck< 'getServerRole', - { userId: string; role: ServerRoles } + UserContext & { role: ServerRoles } > = (loaders) => async ({ userId, role: requiredServerRole }) => { const userServerRole = await loaders.getServerRole({ userId }) - if (userServerRole.isErr) { - switch (userServerRole.error.code) { - case 'ServerRoleNotFound': - return false - default: - throwUncoveredError(userServerRole.error.code) - } - } - return isMinimumServerRole(userServerRole.value, requiredServerRole) + if (!userServerRole) return false + return isMinimumServerRole(userServerRole, requiredServerRole) } export const canUseAdminOverride: AuthPolicyCheck< 'getEnv' | 'getServerRole', - { userId: string } + UserContext > = (loaders) => async ({ userId }) => { diff --git a/packages/shared/src/authz/checks/workspaceRole.spec.ts b/packages/shared/src/authz/checks/workspaceRole.spec.ts index df4caf6e4..ddb7bf777 100644 --- a/packages/shared/src/authz/checks/workspaceRole.spec.ts +++ b/packages/shared/src/authz/checks/workspaceRole.spec.ts @@ -1,39 +1,11 @@ import { describe, expect, it } from 'vitest' import { hasAnyWorkspaceRole, requireMinimumWorkspaceRole } from './workspaceRole.js' import cryptoRandomString from 'crypto-random-string' -import { err, ok } from 'true-myth/result' -import { - WorkspaceRoleNotFoundError, - ProjectRoleNotFoundError -} from '../domain/authErrors.js' describe('hasAnyWorkspaceRole returns a function, that', () => { - it('throws uncoveredError for unexpected loader errors', async () => { - await expect( - hasAnyWorkspaceRole({ - // @ts-expect-error deliberately testing an unexpected loader error - getWorkspaceRole: async () => err(new ProjectRoleNotFoundError()) - })({ - userId: cryptoRandomString({ length: 10 }), - workspaceId: cryptoRandomString({ length: 10 }) - }) - ).rejects.toThrowError(/Uncovered error/) - }) - it.each([WorkspaceRoleNotFoundError])( - 'turns expected loader error $code into false ', - async (loaderError) => { - const result = await hasAnyWorkspaceRole({ - getWorkspaceRole: async () => err(new loaderError()) - })({ - userId: cryptoRandomString({ length: 10 }), - workspaceId: cryptoRandomString({ length: 10 }) - }) - expect(result).toEqual(false) - } - ) it('returns false if the user has no role', async () => { const result = await hasAnyWorkspaceRole({ - getWorkspaceRole: async () => err(new WorkspaceRoleNotFoundError()) + getWorkspaceRole: async () => null })({ userId: cryptoRandomString({ length: 9 }), workspaceId: cryptoRandomString({ length: 9 }) @@ -42,7 +14,7 @@ describe('hasAnyWorkspaceRole returns a function, that', () => { }) it('returns true if the user has a role', async () => { const result = await hasAnyWorkspaceRole({ - getWorkspaceRole: async () => ok('workspace:member') + getWorkspaceRole: async () => 'workspace:member' })({ userId: cryptoRandomString({ length: 9 }), workspaceId: cryptoRandomString({ length: 9 }) @@ -52,34 +24,19 @@ describe('hasAnyWorkspaceRole returns a function, that', () => { }) describe('requireMinimumWorkspaceRole returns a function, that', () => { - it('throws uncoveredError for unexpected loader errors', async () => { - await expect( - requireMinimumWorkspaceRole({ - // @ts-expect-error deliberately testing an unexpected loader error - getWorkspaceRole: async () => err(new ProjectRoleNotFoundError()) - })({ - userId: cryptoRandomString({ length: 10 }), - workspaceId: cryptoRandomString({ length: 10 }), - role: 'workspace:member' - }) - ).rejects.toThrowError(/Uncovered error/) + it('turns non existing workspace role into false ', async () => { + const result = await requireMinimumWorkspaceRole({ + getWorkspaceRole: async () => null + })({ + userId: cryptoRandomString({ length: 10 }), + workspaceId: cryptoRandomString({ length: 10 }), + role: 'workspace:member' + }) + expect(result).toEqual(false) }) - it.each([WorkspaceRoleNotFoundError])( - 'turns expected loader error $code into false ', - async (loaderError) => { - const result = await requireMinimumWorkspaceRole({ - getWorkspaceRole: async () => err(new loaderError()) - })({ - userId: cryptoRandomString({ length: 10 }), - workspaceId: cryptoRandomString({ length: 10 }), - role: 'workspace:member' - }) - expect(result).toEqual(false) - } - ) it('returns false if user is below target role', async () => { const result = await requireMinimumWorkspaceRole({ - getWorkspaceRole: () => Promise.resolve(ok('workspace:member')) + getWorkspaceRole: async () => 'workspace:member' })({ userId: cryptoRandomString({ length: 10 }), workspaceId: cryptoRandomString({ length: 10 }), @@ -89,7 +46,7 @@ describe('requireMinimumWorkspaceRole returns a function, that', () => { }) it('returns true if user matches target role', async () => { const result = await requireMinimumWorkspaceRole({ - getWorkspaceRole: () => Promise.resolve(ok('workspace:member')) + getWorkspaceRole: async () => 'workspace:member' })({ userId: cryptoRandomString({ length: 10 }), workspaceId: cryptoRandomString({ length: 10 }), @@ -99,7 +56,7 @@ describe('requireMinimumWorkspaceRole returns a function, that', () => { }) it('returns true if user exceeds target role', async () => { const result = await requireMinimumWorkspaceRole({ - getWorkspaceRole: () => Promise.resolve(ok('workspace:admin')) + getWorkspaceRole: async () => 'workspace:admin' })({ userId: cryptoRandomString({ length: 10 }), workspaceId: cryptoRandomString({ length: 10 }), diff --git a/packages/shared/src/authz/checks/workspaceRole.ts b/packages/shared/src/authz/checks/workspaceRole.ts index fa494e250..d93eab400 100644 --- a/packages/shared/src/authz/checks/workspaceRole.ts +++ b/packages/shared/src/authz/checks/workspaceRole.ts @@ -1,25 +1,18 @@ import { WorkspaceRoles } from '../../core/constants.js' -import { throwUncoveredError } from '../../core/index.js' +import { UserContext, WorkspaceContext } from '../domain/context.js' import { isMinimumWorkspaceRole } from '../domain/logic/roles.js' import { AuthPolicyCheck } from '../domain/policies.js' export const requireMinimumWorkspaceRole: AuthPolicyCheck< 'getWorkspaceRole', - { userId: string; workspaceId: string; role: WorkspaceRoles } + UserContext & WorkspaceContext & { role: WorkspaceRoles } > = (loaders) => async ({ userId, workspaceId, role: requiredWorkspaceRole }) => { const userWorkspaceRole = await loaders.getWorkspaceRole({ userId, workspaceId }) - if (userWorkspaceRole.isErr) { - switch (userWorkspaceRole.error.code) { - case 'WorkspaceRoleNotFound': - return false - default: - throwUncoveredError(userWorkspaceRole.error.code) - } - } + if (!userWorkspaceRole) return false - return isMinimumWorkspaceRole(userWorkspaceRole.value, requiredWorkspaceRole) + return isMinimumWorkspaceRole(userWorkspaceRole, requiredWorkspaceRole) } export const hasAnyWorkspaceRole: AuthPolicyCheck< @@ -29,11 +22,5 @@ export const hasAnyWorkspaceRole: AuthPolicyCheck< (loaders) => async ({ userId, workspaceId }) => { const userWorkspaceRole = await loaders.getWorkspaceRole({ userId, workspaceId }) - if (userWorkspaceRole.isOk) return true - switch (userWorkspaceRole.error.code) { - case 'WorkspaceRoleNotFound': - return false - default: - throwUncoveredError(userWorkspaceRole.error.code) - } + return userWorkspaceRole !== null } diff --git a/packages/shared/src/authz/checks/workspaceSeat.spec.ts b/packages/shared/src/authz/checks/workspaceSeat.spec.ts new file mode 100644 index 000000000..44487b957 --- /dev/null +++ b/packages/shared/src/authz/checks/workspaceSeat.spec.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest' +import { hasEditorSeat } from './workspaceSeat.js' +import { nanoid } from 'nanoid' + +describe('hasEditorSeat returns a function, that', () => { + it('returns false when user has no seats', async () => { + const result = hasEditorSeat({ getWorkspaceSeat: async () => null })({ + userId: nanoid(10), + workspaceId: nanoid(10) + }) + await expect(result).resolves.toBe(false) + }) + it('returns false when user has non editor seats', async () => { + const result = hasEditorSeat({ getWorkspaceSeat: async () => 'viewer' })({ + userId: nanoid(10), + workspaceId: nanoid(10) + }) + await expect(result).resolves.toBe(false) + }) + it('returns true when user has editor seats', async () => { + const result = hasEditorSeat({ getWorkspaceSeat: async () => 'editor' })({ + userId: nanoid(10), + workspaceId: nanoid(10) + }) + await expect(result).resolves.toBe(true) + }) +}) diff --git a/packages/shared/src/authz/checks/workspaceSeat.ts b/packages/shared/src/authz/checks/workspaceSeat.ts new file mode 100644 index 000000000..fee8d1888 --- /dev/null +++ b/packages/shared/src/authz/checks/workspaceSeat.ts @@ -0,0 +1,17 @@ +import { SeatTypes } from '../../core/constants.js' +import { UserContext, WorkspaceContext } from '../domain/context.js' +import { AuthPolicyCheck } from '../domain/policies.js' + +export const hasEditorSeat: AuthPolicyCheck< + 'getWorkspaceSeat', + UserContext & WorkspaceContext +> = + (loaders) => + async ({ userId, workspaceId }) => { + const seat = await loaders.getWorkspaceSeat({ + userId, + workspaceId + }) + if (!seat) return false + return seat === SeatTypes.Editor + } diff --git a/packages/shared/src/authz/domain/authErrors.ts b/packages/shared/src/authz/domain/authErrors.ts index c1d18b9aa..6505d9d40 100644 --- a/packages/shared/src/authz/domain/authErrors.ts +++ b/packages/shared/src/authz/domain/authErrors.ts @@ -1,3 +1,5 @@ +import { WorkspaceLimits } from '../../workspaces/helpers/limits.js' + export type AuthError = { readonly code: ErrorCode readonly message: string @@ -50,14 +52,9 @@ export const ProjectNoAccessError = defineAuthError({ message: 'You do not have access to the project' }) -export const ProjectRoleNotFoundError = defineAuthError({ - code: 'ProjectRoleNotFound', - message: 'Could not resolve your project role' -}) - -export const WorkspaceNotFoundError = defineAuthError({ - code: 'WorkspaceNotFound', - message: 'Workspace not found' +export const WorkspacesNotEnabledError = defineAuthError({ + code: 'WorkspacesNotEnabled', + message: 'This server does not support workspaces' }) export const WorkspaceNoAccessError = defineAuthError({ @@ -65,14 +62,22 @@ export const WorkspaceNoAccessError = defineAuthError({ message: 'You do not have access to the workspace' }) -export const WorkspaceSsoProviderNotFoundError = defineAuthError({ - code: 'WorkspaceSsoProviderNotFound', - message: 'The workspace SSO provider was not found' +export const WorkspaceNotEnoughPermissionsError = defineAuthError({ + code: 'WorkspaceNotEnoughPermissions', + message: 'You do not have enough permissions in the workspace to perform this action' }) -export const WorkspaceSsoSessionNotFoundError = defineAuthError({ - code: 'WorkspaceSsoSessionNotFound', - message: 'Your workspace SSO session was not found' +export const WorkspaceReadOnlyError = defineAuthError({ + code: 'WorkspaceReadOnly', + message: 'The workspace is in a read only mode, upgrade your plan to unlock it' +}) + +export const WorkspaceLimitsReachedError = defineAuthError< + 'WorkspaceLimitsReached', + { limit: keyof WorkspaceLimits } +>({ + code: 'WorkspaceLimitsReached', + message: 'Workspace limits have been reached' }) export const WorkspaceSsoSessionNoAccessError = defineAuthError< @@ -85,9 +90,9 @@ export const WorkspaceSsoSessionNoAccessError = defineAuthError< message: 'Your workspace SSO session is expired or it does not exist' }) -export const WorkspaceRoleNotFoundError = defineAuthError({ - code: 'WorkspaceRoleNotFound', - message: 'The user does not have a role in the workspace' +export const WorkspaceNoEditorSeatError = defineAuthError({ + code: 'WorkspaceNoEditorSeat', + message: 'You need an editor seat to perform this action' }) export const ServerNoAccessError = defineAuthError({ @@ -99,8 +104,3 @@ export const ServerNoSessionError = defineAuthError({ code: 'ServerNoSession', message: 'You are not logged in to this server' }) - -export const ServerRoleNotFoundError = defineAuthError({ - code: 'ServerRoleNotFound', - message: 'Could not resolve your server role' -}) diff --git a/packages/shared/src/authz/domain/context.ts b/packages/shared/src/authz/domain/context.ts new file mode 100644 index 000000000..d9269e4f5 --- /dev/null +++ b/packages/shared/src/authz/domain/context.ts @@ -0,0 +1,4 @@ +export type ProjectContext = { projectId: string } +export type UserContext = { userId: string } +export type MaybeUserContext = { userId?: string } +export type WorkspaceContext = { workspaceId: string } diff --git a/packages/shared/src/authz/domain/core/operations.ts b/packages/shared/src/authz/domain/core/operations.ts index 30e18c45e..a44bb0b85 100644 --- a/packages/shared/src/authz/domain/core/operations.ts +++ b/packages/shared/src/authz/domain/core/operations.ts @@ -1,7 +1,3 @@ -import Result from 'true-myth/result' import { ServerRoles } from '../../../core/constants.js' -import { ServerRoleNotFoundError } from '../authErrors.js' -export type GetServerRole = (args: { - userId: string -}) => Promise>> +export type GetServerRole = (args: { userId: string }) => Promise diff --git a/packages/shared/src/authz/domain/loaders.ts b/packages/shared/src/authz/domain/loaders.ts index 7ee96371d..3b9593da6 100644 --- a/packages/shared/src/authz/domain/loaders.ts +++ b/packages/shared/src/authz/domain/loaders.ts @@ -5,7 +5,11 @@ import type { GetProject, GetProjectRole } from './projects/operations.js' import type { GetEnv, GetWorkspace, + GetWorkspaceLimits, + GetWorkspacePlan, + GetWorkspaceProjectCount, GetWorkspaceRole, + GetWorkspaceSeat, GetWorkspaceSsoProvider, GetWorkspaceSsoSession } from './workspaces/operations.js' @@ -45,6 +49,10 @@ export const AuthCheckContextLoaderKeys = { getServerRole: 'getServerRole', getWorkspace: 'getWorkspace', getWorkspaceRole: 'getWorkspaceRole', + getWorkspaceSeat: 'getWorkspaceSeat', + getWorkspaceProjectCount: 'getWorkspaceProjectCount', + getWorkspacePlan: 'getWorkspacePlan', + getWorkspaceLimits: 'getWorkspaceLimits', getWorkspaceSsoProvider: 'getWorkspaceSsoProvider', getWorkspaceSsoSession: 'getWorkspaceSsoSession' } @@ -60,6 +68,10 @@ export type AllAuthCheckContextLoaders = AuthContextLoaderMappingDefinition<{ getServerRole: GetServerRole getWorkspace: GetWorkspace getWorkspaceRole: GetWorkspaceRole + getWorkspaceLimits: GetWorkspaceLimits + getWorkspacePlan: GetWorkspacePlan + getWorkspaceSeat: GetWorkspaceSeat + getWorkspaceProjectCount: GetWorkspaceProjectCount getWorkspaceSsoProvider: GetWorkspaceSsoProvider getWorkspaceSsoSession: GetWorkspaceSsoSession }> diff --git a/packages/shared/src/authz/domain/policies.ts b/packages/shared/src/authz/domain/policies.ts index 878d2a1fd..d86647f02 100644 --- a/packages/shared/src/authz/domain/policies.ts +++ b/packages/shared/src/authz/domain/policies.ts @@ -4,10 +4,6 @@ import { AuthError } from './authErrors.js' import { AuthCheckContextLoaderKeys, AuthCheckContextLoaders } from './loaders.js' import Maybe from 'true-myth/maybe' -export type ProjectContext = { projectId: string } -export type UserContext = { userId: string } -export type MaybeUserContext = { userId?: string } - // a complete policy always returns a full result export type AuthPolicy< LoaderKeys extends AuthCheckContextLoaderKeys, diff --git a/packages/shared/src/authz/domain/projects/operations.ts b/packages/shared/src/authz/domain/projects/operations.ts index c0cf7bf5f..1bd6cf682 100644 --- a/packages/shared/src/authz/domain/projects/operations.ts +++ b/packages/shared/src/authz/domain/projects/operations.ts @@ -1,25 +1,9 @@ -import { Result } from 'true-myth/result' import { StreamRoles } from '../../../core/constants.js' import { Project } from './types.js' -import { - ProjectNoAccessError, - ProjectNotFoundError, - ProjectRoleNotFoundError, - WorkspaceSsoSessionNoAccessError -} from '../authErrors.js' -export type GetProject = (args: { - projectId: string -}) => Promise< - Result< - Project, - | InstanceType - | InstanceType - | InstanceType - > -> +export type GetProject = (args: { projectId: string }) => Promise export type GetProjectRole = (args: { userId: string projectId: string -}) => Promise>> +}) => Promise diff --git a/packages/shared/src/authz/domain/workspaces/operations.ts b/packages/shared/src/authz/domain/workspaces/operations.ts index bdf3a5adb..8edc0f223 100644 --- a/packages/shared/src/authz/domain/workspaces/operations.ts +++ b/packages/shared/src/authz/domain/workspaces/operations.ts @@ -1,43 +1,36 @@ -import Result from 'true-myth/result' -import { WorkspaceRoles } from '../../../core/constants.js' +import { WorkspaceRoles, WorkspaceSeatType } from '../../../core/constants.js' import { FeatureFlags } from '../../../environment/index.js' +import { WorkspaceLimits } from '../../../workspaces/helpers/limits.js' +import { WorkspacePlan } from '../../../workspaces/index.js' +import { UserContext, WorkspaceContext } from '../context.js' import { Workspace, WorkspaceSsoProvider, WorkspaceSsoSession } from './types.js' -import { - WorkspaceNoAccessError, - WorkspaceNotFoundError, - WorkspaceRoleNotFoundError, - WorkspaceSsoProviderNotFoundError, - WorkspaceSsoSessionNoAccessError, - WorkspaceSsoSessionNotFoundError -} from '../authErrors.js' -export type GetWorkspace = (args: { - workspaceId: string -}) => Promise< - Result< - Workspace, - | InstanceType - | InstanceType - | InstanceType - > -> +export type GetWorkspace = (args: WorkspaceContext) => Promise -export type GetWorkspaceRole = (args: { - userId: string - workspaceId: string -}) => Promise>> +export type GetWorkspaceRole = ( + args: UserContext & WorkspaceContext +) => Promise -export type GetWorkspaceSsoProvider = (args: { - workspaceId: string -}) => Promise< - Result> -> +export type GetWorkspaceLimits = ( + args: WorkspaceContext +) => Promise -export type GetWorkspaceSsoSession = (args: { - userId: string - workspaceId: string -}) => Promise< - Result> -> +export type GetWorkspacePlan = (args: WorkspaceContext) => Promise + +export type GetWorkspaceProjectCount = ( + args: WorkspaceContext +) => Promise + +export type GetWorkspaceSeat = ( + args: UserContext & WorkspaceContext +) => Promise + +export type GetWorkspaceSsoProvider = ( + args: WorkspaceContext +) => Promise + +export type GetWorkspaceSsoSession = ( + args: UserContext & WorkspaceContext +) => Promise export type GetEnv = () => Promise diff --git a/packages/shared/src/authz/fragments/workspaceSso.spec.ts b/packages/shared/src/authz/fragments/workspaceSso.spec.ts index 746883251..7a37feaa3 100644 --- a/packages/shared/src/authz/fragments/workspaceSso.spec.ts +++ b/packages/shared/src/authz/fragments/workspaceSso.spec.ts @@ -3,93 +3,62 @@ import { maybeMemberRoleWithValidSsoSessionIfNeeded } from './workspaceSso.js' import cryptoRandomString from 'crypto-random-string' import { err, ok } from 'true-myth/result' import { - ProjectNotFoundError, WorkspaceNoAccessError, - WorkspaceNotFoundError, - WorkspaceRoleNotFoundError, - WorkspaceSsoProviderNotFoundError, - WorkspaceSsoSessionNoAccessError, - WorkspaceSsoSessionNotFoundError + WorkspaceSsoSessionNoAccessError } from '../domain/authErrors.js' import { just, nothing } from 'true-myth/maybe' describe('maybeMemberRoleWithValidSsoSessionIfNeeded returns a function, that', () => { - it.each([new WorkspaceNoAccessError(), new WorkspaceNotFoundError()])( - 'remaps workspace loader error $code into WorkspaceNoAccessError', - async (expectedError) => { - const result = maybeMemberRoleWithValidSsoSessionIfNeeded({ - getWorkspace: async () => err(expectedError), - getWorkspaceRole: async () => err(new WorkspaceRoleNotFoundError()), - getWorkspaceSsoProvider: async () => - err(new WorkspaceSsoProviderNotFoundError()), - getWorkspaceSsoSession: async () => err(new WorkspaceSsoSessionNotFoundError()) - })({ - userId: cryptoRandomString({ length: 10 }), - workspaceId: cryptoRandomString({ length: 10 }) - }) - await expect(result).resolves.toStrictEqual( - just(err(new WorkspaceNoAccessError())) - ) - } - ) - it('returns workspace loader error sso error completely', async () => { - const ssoError = new WorkspaceSsoSessionNoAccessError({ - payload: { workspaceSlug: cryptoRandomString({ length: 10 }) } - }) + it('hides non existing workspaces behind a WorkspaceNoAccessError', async () => { const result = maybeMemberRoleWithValidSsoSessionIfNeeded({ - getWorkspace: async () => err(ssoError), - getWorkspaceRole: async () => err(new WorkspaceRoleNotFoundError()), - getWorkspaceSsoProvider: async () => err(new WorkspaceSsoProviderNotFoundError()), - getWorkspaceSsoSession: async () => err(new WorkspaceSsoSessionNotFoundError()) - })({ - userId: cryptoRandomString({ length: 10 }), - workspaceId: cryptoRandomString({ length: 10 }) - }) - await expect(result).resolves.toStrictEqual(just(err(ssoError))) - }) - it('throws Uncovered error for unknown loader errors', async () => { - await expect( - maybeMemberRoleWithValidSsoSessionIfNeeded({ - // @ts-expect-error testing the unexpected error case here - getWorkspace: async () => err(new WorkspaceSsoProviderNotFoundError()), - getWorkspaceRole: async () => err(new WorkspaceRoleNotFoundError()), - getWorkspaceSsoProvider: async () => - err(new WorkspaceSsoProviderNotFoundError()), - getWorkspaceSsoSession: async () => err(new WorkspaceSsoSessionNotFoundError()) - })({ - userId: cryptoRandomString({ length: 10 }), - workspaceId: cryptoRandomString({ length: 10 }) - }) - ).rejects.toThrowError(/Uncovered error/) - }) - it('returns nothing if user does not have a workspace role', async () => { - const result = await maybeMemberRoleWithValidSsoSessionIfNeeded({ - getWorkspace: async () => { - return ok({ - id: 'aaa', - slug: 'bbb' - }) + getWorkspace: async () => null, + getWorkspaceRole: async () => { + expect.fail() }, - getWorkspaceRole: async () => err(new WorkspaceRoleNotFoundError()), - getWorkspaceSsoProvider: async () => err(new WorkspaceSsoProviderNotFoundError()), - getWorkspaceSsoSession: async () => err(new WorkspaceSsoSessionNotFoundError()) + getWorkspaceSsoProvider: async () => { + expect.fail() + }, + getWorkspaceSsoSession: async () => { + expect.fail() + } })({ userId: cryptoRandomString({ length: 10 }), workspaceId: cryptoRandomString({ length: 10 }) }) - expect(result).toStrictEqual(nothing()) + await expect(result).resolves.toStrictEqual(just(err(new WorkspaceNoAccessError()))) + }) + it('returns WorkspaceNoAccessError if the user does not have a workspace role', async () => { + const result = await maybeMemberRoleWithValidSsoSessionIfNeeded({ + getWorkspace: async () => ({ + id: 'aaa', + slug: 'bbb' + }), + getWorkspaceRole: async () => null, + getWorkspaceSsoProvider: async () => { + expect.fail() + }, + getWorkspaceSsoSession: async () => { + expect.fail() + } + })({ + userId: cryptoRandomString({ length: 10 }), + workspaceId: cryptoRandomString({ length: 10 }) + }) + expect(result).toStrictEqual(just(err(new WorkspaceNoAccessError()))) }) it('returns nothing if user does not have a minimum workspace:member role', async () => { const result = await maybeMemberRoleWithValidSsoSessionIfNeeded({ - getWorkspace: async () => { - return ok({ - id: 'aaa', - slug: 'bbb' - }) + getWorkspace: async () => ({ + id: 'aaa', + slug: 'bbb' + }), + getWorkspaceRole: async () => 'workspace:guest', + getWorkspaceSsoProvider: async () => { + expect.fail() }, - getWorkspaceRole: async () => ok('workspace:guest'), - getWorkspaceSsoProvider: async () => err(new WorkspaceSsoProviderNotFoundError()), - getWorkspaceSsoSession: async () => err(new WorkspaceSsoSessionNotFoundError()) + getWorkspaceSsoSession: async () => { + expect.fail() + } })({ userId: cryptoRandomString({ length: 10 }), workspaceId: cryptoRandomString({ length: 10 }) @@ -98,70 +67,32 @@ describe('maybeMemberRoleWithValidSsoSessionIfNeeded returns a function, that', }) it('returns just(ok()) if user is a member and workspace has no SSO provider', async () => { const result = await maybeMemberRoleWithValidSsoSessionIfNeeded({ - getWorkspace: async () => { - return ok({ - id: 'aaa', - slug: 'bbb' - }) - }, - getWorkspaceRole: async () => ok('workspace:member'), - getWorkspaceSsoProvider: async () => err(new WorkspaceSsoProviderNotFoundError()), - getWorkspaceSsoSession: async () => err(new WorkspaceSsoSessionNotFoundError()) + getWorkspace: async () => ({ + id: 'aaa', + slug: 'bbb' + }), + getWorkspaceRole: async () => 'workspace:member', + getWorkspaceSsoProvider: async () => null, + getWorkspaceSsoSession: async () => { + expect.fail() + } })({ userId: cryptoRandomString({ length: 10 }), workspaceId: cryptoRandomString({ length: 10 }) }) expect(result).toStrictEqual(just(ok())) }) - it('throws uncovered error for unexpected ssoProvider loader errors', async () => { - const result = maybeMemberRoleWithValidSsoSessionIfNeeded({ - getWorkspace: async () => { - return ok({ - id: 'aaa', - slug: 'bbb' - }) - }, - getWorkspaceRole: async () => ok('workspace:member'), - // @ts-expect-error testing uncovered errors - getWorkspaceSsoProvider: async () => err(new ProjectNotFoundError()), - getWorkspaceSsoSession: async () => err(new WorkspaceSsoSessionNotFoundError()) - })({ - userId: cryptoRandomString({ length: 10 }), - workspaceId: cryptoRandomString({ length: 10 }) - }) - await expect(result).rejects.toThrowError(/Uncovered error/) - }) - it('throws uncovered error for unexpected ssoSession loader errors', async () => { - const result = maybeMemberRoleWithValidSsoSessionIfNeeded({ - getWorkspace: async () => { - return ok({ - id: 'aaa', - slug: 'bbb' - }) - }, - getWorkspaceRole: async () => ok('workspace:member'), - getWorkspaceSsoProvider: async () => - ok({ providerId: cryptoRandomString({ length: 10 }) }), - // @ts-expect-error testing uncovered errors - getWorkspaceSsoSession: async () => err(new ProjectNotFoundError()) - })({ - userId: cryptoRandomString({ length: 10 }), - workspaceId: cryptoRandomString({ length: 10 }) - }) - await expect(result).rejects.toThrowError(/Uncovered error/) - }) it('returns WorkspaceSsoSessionInvalidError if user does not have an SSO session', async () => { const result = maybeMemberRoleWithValidSsoSessionIfNeeded({ - getWorkspace: async () => { - return ok({ - id: 'aaa', - slug: 'bbb' - }) - }, - getWorkspaceRole: async () => ok('workspace:member'), - getWorkspaceSsoProvider: async () => - ok({ providerId: cryptoRandomString({ length: 10 }) }), - getWorkspaceSsoSession: async () => err(new WorkspaceSsoSessionNotFoundError()) + getWorkspace: async () => ({ + id: 'aaa', + slug: 'bbb' + }), + getWorkspaceRole: async () => 'workspace:member', + getWorkspaceSsoProvider: async () => ({ + providerId: cryptoRandomString({ length: 10 }) + }), + getWorkspaceSsoSession: async () => null })({ userId: cryptoRandomString({ length: 10 }), workspaceId: cryptoRandomString({ length: 10 }) @@ -181,16 +112,15 @@ describe('maybeMemberRoleWithValidSsoSessionIfNeeded returns a function, that', validUntil.setDate(validUntil.getDate() - 1) const result = await maybeMemberRoleWithValidSsoSessionIfNeeded({ - getWorkspace: async () => { - return ok({ - id: 'aaa', - slug: 'bbb' - }) - }, - getWorkspaceRole: async () => ok('workspace:member'), - getWorkspaceSsoProvider: async () => - ok({ providerId: cryptoRandomString({ length: 10 }) }), - getWorkspaceSsoSession: async () => ok({ providerId, validUntil, userId }) + getWorkspace: async () => ({ + id: 'aaa', + slug: 'bbb' + }), + getWorkspaceRole: async () => 'workspace:member', + getWorkspaceSsoProvider: async () => ({ + providerId: cryptoRandomString({ length: 10 }) + }), + getWorkspaceSsoSession: async () => ({ providerId, validUntil, userId }) })({ userId, workspaceId @@ -210,16 +140,15 @@ describe('maybeMemberRoleWithValidSsoSessionIfNeeded returns a function, that', validUntil.setDate(validUntil.getDate() + 100) const result = await maybeMemberRoleWithValidSsoSessionIfNeeded({ - getWorkspace: async () => { - return ok({ - id: 'aaa', - slug: 'bbb' - }) - }, - getWorkspaceRole: async () => ok('workspace:member'), - getWorkspaceSsoProvider: async () => - ok({ providerId: cryptoRandomString({ length: 10 }) }), - getWorkspaceSsoSession: async () => ok({ providerId, validUntil, userId }) + getWorkspace: async () => ({ + id: 'aaa', + slug: 'bbb' + }), + getWorkspaceRole: async () => 'workspace:member', + getWorkspaceSsoProvider: async () => ({ + providerId: cryptoRandomString({ length: 10 }) + }), + getWorkspaceSsoSession: async () => ({ providerId, validUntil, userId }) })({ userId, workspaceId diff --git a/packages/shared/src/authz/fragments/workspaceSso.ts b/packages/shared/src/authz/fragments/workspaceSso.ts index 2a3cb6da5..cebf94b60 100644 --- a/packages/shared/src/authz/fragments/workspaceSso.ts +++ b/packages/shared/src/authz/fragments/workspaceSso.ts @@ -1,7 +1,9 @@ -import { err, isErr, ok } from 'true-myth/result' -import { throwUncoveredError } from '../../core/helpers/error.js' +import { err, ok } from 'true-myth/result' import { AuthPolicyFragment } from '../domain/policies.js' -import { requireMinimumWorkspaceRole } from '../checks/workspaceRole.js' +import { + hasAnyWorkspaceRole, + requireMinimumWorkspaceRole +} from '../checks/workspaceRole.js' import { just, nothing } from 'true-myth/maybe' import { WorkspaceNoAccessError, @@ -21,64 +23,46 @@ export const maybeMemberRoleWithValidSsoSessionIfNeeded: AuthPolicyFragment< async ({ userId, workspaceId }) => { // Get workspace, so we can resolve its slug for error scenarios const workspace = await loaders.getWorkspace({ workspaceId }) - if (workspace.isErr) { - switch (workspace.error.code) { - case 'WorkspaceNoAccess': - case 'WorkspaceNotFound': - return just(err(new WorkspaceNoAccessError())) - case 'WorkspaceSsoSessionNoAccess': - return just(err(workspace.error)) - default: - throwUncoveredError(workspace.error) - } - } + // hides the fact, that the workspace does not exist + if (!workspace) return just(err(new WorkspaceNoAccessError())) + + const hasAnyRole = await hasAnyWorkspaceRole(loaders)({ userId, workspaceId }) + if (!hasAnyRole) return just(err(new WorkspaceNoAccessError())) + const hasMinimumMemberRole = await requireMinimumWorkspaceRole(loaders)({ userId, workspaceId, role: 'workspace:member' }) + // only members and above need to use sso if (!hasMinimumMemberRole) return nothing() const workspaceSsoProvider = await loaders.getWorkspaceSsoProvider({ workspaceId }) - if (isErr(workspaceSsoProvider)) { - switch (workspaceSsoProvider.error.code) { - case 'WorkspaceSsoProviderNotFound': - // if there is no SSO provider, we can early return here - return just(ok()) - default: - throwUncoveredError(workspaceSsoProvider.error.code) - } - } + if (!workspaceSsoProvider) return just(ok()) const workspaceSsoSession = await loaders.getWorkspaceSsoSession({ userId, workspaceId }) - if (isErr(workspaceSsoSession)) { - switch (workspaceSsoSession.error.code) { - case 'WorkspaceSsoSessionNotFound': - return just( - err( - new WorkspaceSsoSessionNoAccessError({ - payload: { workspaceSlug: workspace.value.slug } - }) - ) - ) - default: - throwUncoveredError(workspaceSsoSession.error.code) - } - } + if (!workspaceSsoSession) + return just( + err( + new WorkspaceSsoSessionNoAccessError({ + payload: { workspaceSlug: workspace.slug } + }) + ) + ) const isExpiredSession = - new Date().getTime() > workspaceSsoSession.value.validUntil.getTime() + new Date().getTime() > workspaceSsoSession.validUntil.getTime() if (isExpiredSession) return just( err( new WorkspaceSsoSessionNoAccessError({ - payload: { workspaceSlug: workspace.value.slug } + payload: { workspaceSlug: workspace.slug } }) ) ) diff --git a/packages/shared/src/authz/helpers/graphql.ts b/packages/shared/src/authz/helpers/graphql.ts new file mode 100644 index 000000000..7847fc87d --- /dev/null +++ b/packages/shared/src/authz/helpers/graphql.ts @@ -0,0 +1,31 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Result } from 'true-myth' +import { AuthError } from '../domain/authErrors.js' + +export type GraphqlPermissionCheckResult = { + authorized: boolean + code: string + message: string + payload: unknown +} + +export const toGraphqlResult = ( + authResult: Result> +): GraphqlPermissionCheckResult => { + if (authResult.isOk) { + return { + authorized: true, + code: 'OK', + message: 'OK', + payload: null + } + } else { + const error = authResult.error + return { + authorized: false, + code: error.code, + message: error.message, + payload: error.payload || null + } + } +} diff --git a/packages/shared/src/authz/index.ts b/packages/shared/src/authz/index.ts index 307d759ad..bc9527c2a 100644 --- a/packages/shared/src/authz/index.ts +++ b/packages/shared/src/authz/index.ts @@ -4,5 +4,5 @@ export { AuthCheckContextLoaders, AuthCheckContextLoaderKeys } from './domain/loaders.js' - +export * from './helpers/graphql.js' export * from './domain/authErrors.js' diff --git a/packages/shared/src/authz/policies/canCreateWorkspaceProject.spec.ts b/packages/shared/src/authz/policies/canCreateWorkspaceProject.spec.ts new file mode 100644 index 000000000..707143c69 --- /dev/null +++ b/packages/shared/src/authz/policies/canCreateWorkspaceProject.spec.ts @@ -0,0 +1,604 @@ +import { assert, describe, expect, it } from 'vitest' +import { + ServerNoAccessError, + ServerNoSessionError, + WorkspaceLimitsReachedError, + WorkspaceNoAccessError, + WorkspaceNoEditorSeatError, + WorkspaceNotEnoughPermissionsError, + WorkspaceReadOnlyError, + WorkspacesNotEnabledError, + WorkspaceSsoSessionNoAccessError +} from '../domain/authErrors.js' +import { nanoid } from 'nanoid' +import { canCreateWorkspaceProjectPolicy } from './canCreateWorkspaceProject.js' +import { parseFeatureFlags } from '../../environment/index.js' +import cryptoRandomString from 'crypto-random-string' +import { WorkspacePlan } from '../../workspaces/index.js' +import { Workspace, WorkspaceSsoProvider } from '../domain/workspaces/types.js' +import { err, ok } from 'true-myth/result' + +const canCreateArgs = () => ({ + userId: cryptoRandomString({ length: 10 }), + workspaceId: cryptoRandomString({ length: 10 }) +}) + +describe('canCreateWorkspaceProjectPolicy creates a function, that handles', () => { + describe('server environment configuration by', () => { + it('forbids creation if the workspaces module is not enabled', async () => { + const result = await canCreateWorkspaceProjectPolicy({ + getEnv: async () => + parseFeatureFlags({ + FF_WORKSPACES_MODULE_ENABLED: 'false' + }), + getServerRole: async () => { + assert.fail() + }, + getWorkspaceRole: async () => { + assert.fail() + }, + getWorkspace: async () => { + assert.fail() + }, + getWorkspaceSeat: async () => { + assert.fail() + }, + getWorkspacePlan: async () => { + assert.fail() + }, + getWorkspaceLimits: async () => { + assert.fail() + }, + getWorkspaceProjectCount: async () => { + assert.fail() + }, + getWorkspaceSsoProvider: async () => { + assert.fail() + }, + getWorkspaceSsoSession: async () => { + assert.fail() + } + })(canCreateArgs()) + + expect(result).toStrictEqual(err(new WorkspacesNotEnabledError())) + }) + }) + + describe('user server roles', () => { + it('forbids creation for unknown users', async () => { + const result = await canCreateWorkspaceProjectPolicy({ + getEnv: async () => parseFeatureFlags({}), + getServerRole: async () => { + return null + }, + getWorkspaceRole: async () => { + assert.fail() + }, + getWorkspace: async () => { + assert.fail() + }, + getWorkspaceSeat: async () => { + assert.fail() + }, + getWorkspacePlan: async () => { + assert.fail() + }, + getWorkspaceLimits: async () => { + assert.fail() + }, + getWorkspaceProjectCount: async () => { + assert.fail() + }, + getWorkspaceSsoProvider: async () => { + assert.fail() + }, + getWorkspaceSsoSession: async () => { + assert.fail() + } + })({ workspaceId: '' }) + + expect(result).toStrictEqual(err(new ServerNoSessionError())) + }) + it('forbids creation for anyone not having minimum server:user role', async () => { + const result = await canCreateWorkspaceProjectPolicy({ + getEnv: async () => parseFeatureFlags({}), + getServerRole: async () => { + return 'server:guest' + }, + getWorkspaceRole: async () => { + assert.fail() + }, + getWorkspace: async () => { + assert.fail() + }, + getWorkspaceSeat: async () => { + assert.fail() + }, + getWorkspacePlan: async () => { + assert.fail() + }, + getWorkspaceLimits: async () => { + assert.fail() + }, + getWorkspaceProjectCount: async () => { + assert.fail() + }, + getWorkspaceSsoProvider: async () => { + assert.fail() + }, + getWorkspaceSsoSession: async () => { + assert.fail() + } + })(canCreateArgs()) + + expect(result).toStrictEqual(err(new ServerNoAccessError())) + }) + }) + + describe('workspace sso', () => { + const workspaceSlug = nanoid() + + it('forbids creation when workspace not found', async () => { + const result = await canCreateWorkspaceProjectPolicy({ + getEnv: async () => parseFeatureFlags({}), + getServerRole: async () => { + return 'server:user' + }, + getWorkspaceRole: async () => { + assert.fail() + }, + getWorkspace: async () => { + return null + }, + getWorkspaceSeat: async () => { + assert.fail() + }, + getWorkspacePlan: async () => { + assert.fail() + }, + getWorkspaceLimits: async () => { + assert.fail() + }, + getWorkspaceProjectCount: async () => { + assert.fail() + }, + getWorkspaceSsoProvider: async () => { + assert.fail() + }, + getWorkspaceSsoSession: async () => { + assert.fail() + } + })(canCreateArgs()) + + expect(result).toStrictEqual(err(new WorkspaceNoAccessError())) + }) + + it('forbids creation when sso session is not found', async () => { + const result = await canCreateWorkspaceProjectPolicy({ + getEnv: async () => parseFeatureFlags({}), + getServerRole: async () => { + return 'server:user' + }, + getWorkspaceRole: async () => { + return 'workspace:member' + }, + getWorkspace: async () => { + return { + slug: workspaceSlug + } as Workspace + }, + getWorkspaceSeat: async () => { + assert.fail() + }, + getWorkspacePlan: async () => { + assert.fail() + }, + getWorkspaceLimits: async () => { + assert.fail() + }, + getWorkspaceProjectCount: async () => { + assert.fail() + }, + getWorkspaceSsoProvider: async () => { + return {} as WorkspaceSsoProvider + }, + getWorkspaceSsoSession: async () => { + return null + } + })(canCreateArgs()) + + expect(result).toStrictEqual( + err( + new WorkspaceSsoSessionNoAccessError({ + payload: { workspaceSlug } + }) + ) + ) + }) + it('throws UncoveredError from unexpected sso session errors') + }) + + describe('user workspace roles', () => { + it('forbids creation for users without a workspace role', async () => { + const result = await canCreateWorkspaceProjectPolicy({ + getEnv: async () => parseFeatureFlags({}), + getServerRole: async () => { + return 'server:user' + }, + getWorkspaceRole: async () => { + return null + }, + getWorkspace: async () => { + return {} as Workspace + }, + getWorkspaceSeat: async () => { + assert.fail() + }, + getWorkspacePlan: async () => { + assert.fail() + }, + getWorkspaceLimits: async () => { + assert.fail() + }, + getWorkspaceProjectCount: async () => { + assert.fail() + }, + getWorkspaceSsoProvider: async () => { + assert.fail() + }, + getWorkspaceSsoSession: async () => { + assert.fail() + } + })(canCreateArgs()) + + expect(result).toStrictEqual(err(new WorkspaceNoAccessError())) + }) + it('WorkspaceNotEnoughPermissionsError for workspace guests', async () => { + const result = await canCreateWorkspaceProjectPolicy({ + getEnv: async () => parseFeatureFlags({}), + getServerRole: async () => { + return 'server:user' + }, + getWorkspaceRole: async () => { + return 'workspace:guest' + }, + getWorkspace: async () => { + return {} as Workspace + }, + getWorkspaceSeat: async () => { + assert.fail() + }, + getWorkspacePlan: async () => { + assert.fail() + }, + getWorkspaceLimits: async () => { + assert.fail() + }, + getWorkspaceProjectCount: async () => { + assert.fail() + }, + getWorkspaceSsoProvider: async () => { + assert.fail() + }, + getWorkspaceSsoSession: async () => { + assert.fail() + } + })(canCreateArgs()) + + expect(result).toStrictEqual( + err( + new WorkspaceNotEnoughPermissionsError({ + message: 'Guests cannot create projects in the workspace' + }) + ) + ) + }) + it('forbids non-editor seats from creating projects', async () => { + const result = await canCreateWorkspaceProjectPolicy({ + getEnv: async () => parseFeatureFlags({}), + getServerRole: async () => { + return 'server:user' + }, + getWorkspaceRole: async () => { + return 'workspace:member' + }, + getWorkspace: async () => { + return {} as Workspace + }, + getWorkspaceSeat: async () => { + return 'viewer' + }, + getWorkspacePlan: async () => { + return { + status: 'valid', + name: 'team' + } as WorkspacePlan + }, + getWorkspaceLimits: async () => { + assert.fail() + }, + getWorkspaceProjectCount: async () => { + assert.fail() + }, + getWorkspaceSsoProvider: async () => { + return null + }, + getWorkspaceSsoSession: async () => { + assert.fail() + } + })(canCreateArgs()) + + expect(result).toStrictEqual(err(new WorkspaceNoEditorSeatError())) + }) + }) + + describe('workspace plans', () => { + it('forbids creation if plan fails to load', async () => { + const result = await canCreateWorkspaceProjectPolicy({ + getEnv: async () => parseFeatureFlags({}), + getServerRole: async () => { + return 'server:user' + }, + getWorkspaceRole: async () => { + return 'workspace:member' + }, + getWorkspace: async () => { + return {} as Workspace + }, + getWorkspaceSeat: async () => { + return 'viewer' + }, + getWorkspacePlan: async () => { + return null + }, + getWorkspaceLimits: async () => { + assert.fail() + }, + getWorkspaceProjectCount: async () => { + assert.fail() + }, + getWorkspaceSsoProvider: async () => { + return null + }, + getWorkspaceSsoSession: async () => { + assert.fail() + } + })(canCreateArgs()) + + expect(result).toStrictEqual(err(new WorkspaceNoAccessError())) + }) + it('forbids creation if plan is read-only', async () => { + const result = await canCreateWorkspaceProjectPolicy({ + getEnv: async () => parseFeatureFlags({}), + getServerRole: async () => { + return 'server:user' + }, + getWorkspaceRole: async () => { + return 'workspace:member' + }, + getWorkspace: async () => { + return {} as Workspace + }, + getWorkspaceSeat: async () => { + return 'viewer' + }, + getWorkspacePlan: async () => { + return { + status: 'expired' + } as WorkspacePlan + }, + getWorkspaceLimits: async () => { + assert.fail() + }, + getWorkspaceProjectCount: async () => { + assert.fail() + }, + getWorkspaceSsoProvider: async () => { + return null + }, + getWorkspaceSsoSession: async () => { + assert.fail() + } + })(canCreateArgs()) + + expect(result).toStrictEqual(err(new WorkspaceReadOnlyError())) + }) + }) + + describe('workspace limits', () => { + it('forbids creation if limits fail to load', async () => { + const result = await canCreateWorkspaceProjectPolicy({ + getEnv: async () => parseFeatureFlags({}), + getServerRole: async () => { + return 'server:user' + }, + getWorkspaceRole: async () => { + return 'workspace:member' + }, + getWorkspace: async () => { + return {} as Workspace + }, + getWorkspaceSeat: async () => { + return 'viewer' + }, + getWorkspacePlan: async () => { + return { + status: 'valid' + } as WorkspacePlan + }, + getWorkspaceLimits: async () => { + return null + }, + getWorkspaceProjectCount: async () => { + assert.fail() + }, + getWorkspaceSsoProvider: async () => { + return null + }, + getWorkspaceSsoSession: async () => { + assert.fail() + } + })(canCreateArgs()) + + expect(result).toStrictEqual(err(new WorkspaceNoAccessError())) + }) + it('allows creation if plan has no limits', async () => { + const result = await canCreateWorkspaceProjectPolicy({ + getEnv: async () => parseFeatureFlags({}), + getServerRole: async () => { + return 'server:user' + }, + getWorkspaceRole: async () => { + return 'workspace:member' + }, + getWorkspace: async () => { + return {} as Workspace + }, + getWorkspaceSeat: async () => { + return 'viewer' + }, + getWorkspacePlan: async () => { + return { + status: 'valid' + } as WorkspacePlan + }, + getWorkspaceLimits: async () => { + return { + projectCount: null, + modelCount: null + } + }, + getWorkspaceProjectCount: async () => { + assert.fail() + }, + getWorkspaceSsoProvider: async () => { + return null + }, + getWorkspaceSsoSession: async () => { + assert.fail() + } + })(canCreateArgs()) + + expect(result).toStrictEqual(ok()) + }) + it('forbids creation if current project count fails to load', async () => { + const result = await canCreateWorkspaceProjectPolicy({ + getEnv: async () => parseFeatureFlags({}), + getServerRole: async () => { + return 'server:user' + }, + getWorkspaceRole: async () => { + return 'workspace:member' + }, + getWorkspace: async () => { + return {} as Workspace + }, + getWorkspaceSeat: async () => { + return 'viewer' + }, + getWorkspacePlan: async () => { + return { + status: 'valid' + } as WorkspacePlan + }, + getWorkspaceLimits: async () => { + return { + projectCount: 10, + modelCount: 50 + } + }, + getWorkspaceProjectCount: async () => { + return null + }, + getWorkspaceSsoProvider: async () => { + return null + }, + getWorkspaceSsoSession: async () => { + assert.fail() + } + })(canCreateArgs()) + + expect(result).toStrictEqual(err(new WorkspaceNoAccessError())) + }) + it('allows creation if new project is within plan limits', async () => { + const result = await canCreateWorkspaceProjectPolicy({ + getEnv: async () => parseFeatureFlags({}), + getServerRole: async () => { + return 'server:user' + }, + getWorkspaceRole: async () => { + return 'workspace:member' + }, + getWorkspace: async () => { + return {} as Workspace + }, + getWorkspaceSeat: async () => { + return 'viewer' + }, + getWorkspacePlan: async () => { + return { + status: 'valid' + } as WorkspacePlan + }, + getWorkspaceLimits: async () => { + return { + projectCount: 10, + modelCount: 50 + } + }, + getWorkspaceProjectCount: async () => { + return 5 + }, + getWorkspaceSsoProvider: async () => { + return null + }, + getWorkspaceSsoSession: async () => { + assert.fail() + } + })(canCreateArgs()) + + expect(result).toStrictEqual(ok()) + }) + it('forbids creation if new project is not within plan limits', async () => { + const result = await canCreateWorkspaceProjectPolicy({ + getEnv: async () => parseFeatureFlags({}), + getServerRole: async () => { + return 'server:user' + }, + getWorkspaceRole: async () => { + return 'workspace:member' + }, + getWorkspace: async () => { + return {} as Workspace + }, + getWorkspaceSeat: async () => { + return 'viewer' + }, + getWorkspacePlan: async () => { + return { + status: 'valid' + } as WorkspacePlan + }, + getWorkspaceLimits: async () => { + return { + projectCount: 10, + modelCount: 50 + } + }, + getWorkspaceProjectCount: async () => { + return 10 + }, + getWorkspaceSsoProvider: async () => { + return null + }, + getWorkspaceSsoSession: async () => { + assert.fail() + } + })(canCreateArgs()) + + expect(result).toStrictEqual( + err(new WorkspaceLimitsReachedError({ payload: { limit: 'projectCount' } })) + ) + }) + }) +}) diff --git a/packages/shared/src/authz/policies/canCreateWorkspaceProject.ts b/packages/shared/src/authz/policies/canCreateWorkspaceProject.ts new file mode 100644 index 000000000..d8aaa0c1a --- /dev/null +++ b/packages/shared/src/authz/policies/canCreateWorkspaceProject.ts @@ -0,0 +1,113 @@ +import { AuthPolicy } from '../domain/policies.js' +import { + ServerNoAccessError, + ServerNoSessionError, + WorkspaceLimitsReachedError, + WorkspaceNoAccessError, + WorkspaceNoEditorSeatError, + WorkspaceNotEnoughPermissionsError, + WorkspaceReadOnlyError, + WorkspacesNotEnabledError, + WorkspaceSsoSessionNoAccessError +} from '../domain/authErrors.js' +import { err, ok } from 'true-myth/result' +import { hasMinimumServerRole } from '../checks/serverRole.js' +import { Roles } from '../../core/constants.js' +import { maybeMemberRoleWithValidSsoSessionIfNeeded } from '../fragments/workspaceSso.js' +import { throwUncoveredError } from '../../core/index.js' +import { hasEditorSeat } from '../checks/workspaceSeat.js' +import { MaybeUserContext, WorkspaceContext } from '../domain/context.js' +import { + isNewWorkspacePlan, + isWorkspacePlanStatusReadOnly +} from '../../workspaces/index.js' + +export const canCreateWorkspaceProjectPolicy: AuthPolicy< + | 'getEnv' + | 'getServerRole' + | 'getWorkspace' + | 'getWorkspaceRole' + | 'getWorkspaceSeat' + | 'getWorkspacePlan' + | 'getWorkspaceLimits' + | 'getWorkspaceProjectCount' + | 'getWorkspaceSsoProvider' + | 'getWorkspaceSsoSession', + MaybeUserContext & WorkspaceContext, + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType +> = + (loaders) => + async ({ userId, workspaceId }) => { + const env = await loaders.getEnv() + if (!userId) return err(new ServerNoSessionError()) + if (!env.FF_WORKSPACES_MODULE_ENABLED) return err(new WorkspacesNotEnabledError()) + + const isActiveServerUser = await hasMinimumServerRole(loaders)({ + userId, + role: Roles.Server.User + }) + if (!isActiveServerUser) return err(new ServerNoAccessError()) + + const memberWithSsoSession = await maybeMemberRoleWithValidSsoSessionIfNeeded( + loaders + )({ + userId, + workspaceId + }) + // guests cannot create projects in the workspace + if (memberWithSsoSession.isNothing) + return err( + new WorkspaceNotEnoughPermissionsError({ + message: 'Guests cannot create projects in the workspace' + }) + ) + + // if sso session is not valid, return errors + if (memberWithSsoSession.value.isErr) { + switch (memberWithSsoSession.value.error.code) { + case 'WorkspaceNoAccess': + case 'WorkspaceSsoSessionNoAccess': + return err(memberWithSsoSession.value.error) + default: + throwUncoveredError(memberWithSsoSession.value.error) + } + } + + const workspacePlan = await loaders.getWorkspacePlan({ workspaceId }) + if (!workspacePlan) return err(new WorkspaceNoAccessError()) + + if (isWorkspacePlanStatusReadOnly(workspacePlan.status)) + return err(new WorkspaceReadOnlyError()) + + if (isNewWorkspacePlan(workspacePlan.name)) { + const isEditor = await hasEditorSeat(loaders)({ + userId, + workspaceId + }) + if (!isEditor) return err(new WorkspaceNoEditorSeatError()) + } + + const workspaceLimits = await loaders.getWorkspaceLimits({ workspaceId }) + if (!workspaceLimits) return err(new WorkspaceNoAccessError()) + + // no limits imposed + if (workspaceLimits.projectCount === null) return ok() + const currentProjectCount = await loaders.getWorkspaceProjectCount({ + workspaceId + }) + + // this will not happen in practice + if (currentProjectCount === null) return err(new WorkspaceNoAccessError()) + + return currentProjectCount < workspaceLimits.projectCount + ? ok() + : err(new WorkspaceLimitsReachedError({ payload: { limit: 'projectCount' } })) + } diff --git a/packages/shared/src/authz/policies/canQueryProject.spec.ts b/packages/shared/src/authz/policies/canReadProject.spec.ts similarity index 65% rename from packages/shared/src/authz/policies/canQueryProject.spec.ts rename to packages/shared/src/authz/policies/canReadProject.spec.ts index b285c45ed..a28fc34e4 100644 --- a/packages/shared/src/authz/policies/canQueryProject.spec.ts +++ b/packages/shared/src/authz/policies/canReadProject.spec.ts @@ -1,77 +1,39 @@ import { describe, expect, it, assert } from 'vitest' -import { canQueryProjectPolicy } from './canQueryProject.js' +import { canReadProjectPolicy } from './canReadProject.js' import { parseFeatureFlags } from '../../environment/index.js' import crs from 'crypto-random-string' import { Roles } from '../../core/constants.js' import { ProjectNoAccessError, ProjectNotFoundError, - ProjectRoleNotFoundError, ServerNoAccessError, ServerNoSessionError, - ServerRoleNotFoundError, WorkspaceNoAccessError, - WorkspaceRoleNotFoundError, - WorkspaceSsoProviderNotFoundError, - WorkspaceSsoSessionNoAccessError, - WorkspaceSsoSessionNotFoundError + WorkspaceSsoSessionNoAccessError } from '../domain/authErrors.js' import { getProjectFake } from '../../tests/fakes.js' import { err, ok } from 'true-myth/result' import cryptoRandomString from 'crypto-random-string' import { AuthCheckContextLoaders } from '../domain/loaders.js' -const canQueryProjectArgs = () => { +const canReadProjectArgs = () => { const projectId = crs({ length: 10 }) const userId = crs({ length: 10 }) return { projectId, userId } } -const getWorkspace: AuthCheckContextLoaders['getWorkspace'] = async () => - ok({ - id: 'aaa', - slug: 'bbb' - }) +const getWorkspace: AuthCheckContextLoaders['getWorkspace'] = async () => ({ + id: 'aaa', + slug: 'bbb' +}) -describe('canQueryProjectPolicy creates a function, that handles ', () => { - describe('project loader errors', () => { - it.each([ - ProjectNoAccessError, - ProjectNotFoundError, - WorkspaceSsoSessionNoAccessError - ])('expected $code error by returning the error', async (expectedError) => { - const result = await canQueryProjectPolicy({ - getEnv: async () => parseFeatureFlags({}), - getProject: async () => - err(new expectedError({ payload: { workspaceSlug: 'bbb' } })), - getProjectRole: () => { - assert.fail() - }, - getServerRole: () => { - assert.fail() - }, - getWorkspace, - getWorkspaceRole: () => { - assert.fail() - }, - getWorkspaceSsoSession: () => { - assert.fail() - }, - getWorkspaceSsoProvider: () => { - assert.fail() - } - })(canQueryProjectArgs()) - - expect(result).toStrictEqual( - err(new expectedError({ payload: { workspaceSlug: 'bbb' } })) - ) - }) - it('unexpected error by throwing UncoveredError', async () => { - const result = canQueryProjectPolicy({ +describe('canReadProjectPolicy creates a function, that handles ', () => { + describe('project loader', () => { + it('converts not found projects into ProjectNotFoundError', async () => { + const result = canReadProjectPolicy({ getWorkspace, getEnv: async () => parseFeatureFlags({}), - // @ts-expect-error testing uncovered error handling - getProject: async () => err(ProjectRoleNotFoundError), + getProject: async () => null, getProjectRole: () => { assert.fail() }, @@ -87,14 +49,14 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { getWorkspaceSsoProvider: () => { assert.fail() } - })(canQueryProjectArgs()) + })(canReadProjectArgs()) - await expect(result).rejects.toThrowError(/Uncovered error/) + await expect(result).resolves.toStrictEqual(err(new ProjectNotFoundError())) }) }) describe('project visibility', () => { it('allows anyone on a public project', async () => { - const canQueryProject = canQueryProjectPolicy({ + const canReadProject = canReadProjectPolicy({ getEnv: async () => parseFeatureFlags({}), getProject: getProjectFake({ isPublic: true }), getProjectRole: () => { @@ -115,11 +77,11 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { assert.fail() } }) - const canQuery = await canQueryProject(canQueryProjectArgs()) + const canQuery = await canReadProject(canReadProjectArgs()) expect(canQuery.isOk).toBe(true) }) it('allows anyone on a linkShareable project', async () => { - const canQueryProject = canQueryProjectPolicy({ + const canReadProject = canReadProjectPolicy({ getEnv: async () => parseFeatureFlags({}), getProject: getProjectFake({ isDiscoverable: true }), getProjectRole: () => { @@ -139,19 +101,19 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { assert.fail() } }) - const canQuery = await canQueryProject(canQueryProjectArgs()) + const canQuery = await canReadProject(canReadProjectArgs()) expect(canQuery.isOk).toBe(true) }) }) describe('server roles', () => { it('allows access for archived server users with a project role on a public project', async () => { - const result = canQueryProjectPolicy({ + const result = canReadProjectPolicy({ getEnv: async () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'false' }), getProject: getProjectFake({ isDiscoverable: false, isPublic: true }), - getProjectRole: async () => ok(Roles.Stream.Owner), - getServerRole: async () => ok(Roles.Server.ArchivedUser), + getProjectRole: async () => Roles.Stream.Owner, + getServerRole: async () => Roles.Server.ArchivedUser, getWorkspace, getWorkspaceRole: () => { assert.fail() @@ -162,16 +124,16 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { getWorkspaceSsoProvider: () => { assert.fail() } - })(canQueryProjectArgs()) + })(canReadProjectArgs()) await expect(result).resolves.toStrictEqual(ok()) }) it('does not allow access for archived server users with a project role', async () => { - const result = canQueryProjectPolicy({ + const result = canReadProjectPolicy({ getEnv: async () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'false' }), getProject: getProjectFake({ isDiscoverable: false, isPublic: false }), - getProjectRole: async () => ok(Roles.Stream.Owner), - getServerRole: async () => ok(Roles.Server.ArchivedUser), + getProjectRole: async () => Roles.Stream.Owner, + getServerRole: async () => Roles.Server.ArchivedUser, getWorkspace, getWorkspaceRole: () => { assert.fail() @@ -182,16 +144,16 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { getWorkspaceSsoProvider: () => { assert.fail() } - })(canQueryProjectArgs()) + })(canReadProjectArgs()) await expect(result).resolves.toStrictEqual(err(new ServerNoAccessError())) }) it('does not allow access for non public projects for unknown users', async () => { - const result = canQueryProjectPolicy({ + const result = canReadProjectPolicy({ getEnv: async () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'false' }), getProject: getProjectFake({ isDiscoverable: false, isPublic: false }), - getProjectRole: async () => ok(Roles.Stream.Owner), - getServerRole: async () => err(new ServerRoleNotFoundError()), + getProjectRole: async () => Roles.Stream.Owner, + getServerRole: async () => null, getWorkspace, getWorkspaceRole: () => { @@ -212,12 +174,12 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { it.each(Object.values(Roles.Stream))( 'allows access for active server users to private projects with %s role', async (role) => { - const canQueryProject = canQueryProjectPolicy({ + const canReadProject = canReadProjectPolicy({ getEnv: async () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'false' }), getProject: getProjectFake({ isDiscoverable: false, isPublic: false }), - getProjectRole: async () => ok(role), - getServerRole: async () => ok('server:user'), + getProjectRole: async () => role, + getServerRole: async () => Roles.Server.User, getWorkspace, getWorkspaceRole: () => { assert.fail() @@ -229,18 +191,18 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { assert.fail() } }) - const canQuery = await canQueryProject(canQueryProjectArgs()) + const canQuery = await canReadProject(canReadProjectArgs()) expect(canQuery.isOk).toBe(true) } ) it('does not allow access to private projects without a project role', async () => { - const result = canQueryProjectPolicy({ + const result = canReadProjectPolicy({ getEnv: async () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'false' }), getProject: getProjectFake({ isDiscoverable: false, isPublic: false }), - getProjectRole: async () => err(new ProjectRoleNotFoundError()), + getProjectRole: async () => null, getWorkspace, - getServerRole: async () => ok(Roles.Server.Admin), + getServerRole: async () => Roles.Server.Admin, getWorkspaceRole: () => { assert.fail() }, @@ -250,16 +212,16 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { getWorkspaceSsoProvider: () => { assert.fail() } - })(canQueryProjectArgs()) + })(canReadProjectArgs()) await expect(result).resolves.toStrictEqual(err(new ProjectNoAccessError())) }) }) describe('admin override', () => { it('allows server admins without project roles on private projects if admin override is enabled', async () => { - const result = canQueryProjectPolicy({ + const result = canReadProjectPolicy({ getEnv: async () => parseFeatureFlags({ FF_ADMIN_OVERRIDE_ENABLED: 'true' }), getProject: getProjectFake({ isDiscoverable: false, isPublic: false }), - getServerRole: async () => ok(Roles.Server.Admin), + getServerRole: async () => Roles.Server.Admin, getProjectRole: () => { assert.fail() }, @@ -273,20 +235,20 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { getWorkspaceSsoProvider: () => { assert.fail() } - })(canQueryProjectArgs()) + })(canReadProjectArgs()) await expect(result).resolves.toStrictEqual(ok()) }) it('does not allow server admins without project roles on private projects if admin override is disabled', async () => { - const result = canQueryProjectPolicy({ + const result = canReadProjectPolicy({ getEnv: async () => parseFeatureFlags({ FF_ADMIN_OVERRIDE_ENABLED: 'false', FF_WORKSPACES_MODULE_ENABLED: 'false' }), getProject: getProjectFake({ isDiscoverable: false, isPublic: false }), - getServerRole: async () => ok(Roles.Server.Admin), - getProjectRole: async () => err(new ProjectRoleNotFoundError()), + getServerRole: async () => Roles.Server.Admin, + getProjectRole: async () => null, getWorkspace, getWorkspaceRole: () => { @@ -298,13 +260,13 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { getWorkspaceSsoProvider: () => { assert.fail() } - })(canQueryProjectArgs()) + })(canReadProjectArgs()) await expect(result).resolves.toStrictEqual(err(new ProjectNoAccessError())) }) }) describe('the workspace world', () => { it('does not check workspace rules if the workspaces module is not enabled', async () => { - const result = canQueryProjectPolicy({ + const result = canReadProjectPolicy({ getEnv: async () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'false' }), getProject: getProjectFake({ @@ -312,8 +274,8 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { isPublic: false, workspaceId: crs({ length: 10 }) }), - getProjectRole: async () => ok('stream:contributor'), - getServerRole: async () => ok('server:user'), + getProjectRole: async () => Roles.Stream.Contributor, + getServerRole: async () => Roles.Server.User, getWorkspace, getWorkspaceRole: () => { @@ -325,11 +287,11 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { getWorkspaceSsoProvider: () => { assert.fail() } - })(canQueryProjectArgs()) + })(canReadProjectArgs()) await expect(result).resolves.toStrictEqual(ok()) }) it('does not allow project access without a workspace role', async () => { - const result = canQueryProjectPolicy({ + const result = canReadProjectPolicy({ getEnv: async () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'true' @@ -339,22 +301,22 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { isPublic: false, workspaceId: crs({ length: 10 }) }), - getProjectRole: async () => ok('stream:contributor'), - getServerRole: async () => ok('server:user'), + getProjectRole: async () => Roles.Stream.Contributor, + getServerRole: async () => Roles.Server.User, getWorkspace, - getWorkspaceRole: async () => err(new WorkspaceRoleNotFoundError()), + getWorkspaceRole: async () => null, getWorkspaceSsoSession: () => { assert.fail() }, getWorkspaceSsoProvider: () => { assert.fail() } - })(canQueryProjectArgs()) + })(canReadProjectArgs()) await expect(result).resolves.toStrictEqual(err(new WorkspaceNoAccessError())) }) it('allows project access via workspace role if user does not have project role', async () => { - const result = canQueryProjectPolicy({ + const result = canReadProjectPolicy({ getEnv: async () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'true' @@ -364,20 +326,19 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { isPublic: false, workspaceId: crs({ length: 10 }) }), - getProjectRole: async () => err(new ProjectRoleNotFoundError()), - getServerRole: async () => ok('server:user'), - getWorkspaceRole: async () => ok('workspace:admin'), + getProjectRole: async () => null, + getServerRole: async () => Roles.Server.User, + getWorkspaceRole: async () => Roles.Workspace.Admin, getWorkspaceSsoSession: () => { assert.fail() }, getWorkspace, - getWorkspaceSsoProvider: async () => - err(new WorkspaceSsoProviderNotFoundError()) - })(canQueryProjectArgs()) + getWorkspaceSsoProvider: async () => null + })(canReadProjectArgs()) await expect(result).resolves.toStrictEqual(ok()) }) it('does not check SSO sessions if user is workspace guest', async () => { - const result = canQueryProjectPolicy({ + const result = canReadProjectPolicy({ getEnv: async () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'true' @@ -387,21 +348,21 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { isPublic: false, workspaceId: crs({ length: 10 }) }), - getProjectRole: async () => ok('stream:contributor'), - getServerRole: async () => ok('server:user'), + getProjectRole: async () => Roles.Stream.Contributor, + getServerRole: async () => Roles.Server.User, getWorkspace, - getWorkspaceRole: async () => ok('workspace:guest'), + getWorkspaceRole: async () => Roles.Workspace.Guest, getWorkspaceSsoSession: () => { assert.fail() }, getWorkspaceSsoProvider: () => { assert.fail() } - })(canQueryProjectArgs()) + })(canReadProjectArgs()) await expect(result).resolves.toStrictEqual(ok()) }) it('does not check SSO sessions if workspace does not have it enabled', async () => { - const result = canQueryProjectPolicy({ + const result = canReadProjectPolicy({ getEnv: async () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'true' @@ -411,20 +372,19 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { isPublic: false, workspaceId: crs({ length: 10 }) }), - getProjectRole: async () => ok('stream:contributor'), - getServerRole: async () => ok('server:user'), - getWorkspaceRole: async () => ok('workspace:member'), + getProjectRole: async () => Roles.Stream.Contributor, + getServerRole: async () => Roles.Server.User, + getWorkspaceRole: async () => Roles.Workspace.Member, getWorkspace, getWorkspaceSsoSession: () => { assert.fail() }, - getWorkspaceSsoProvider: async () => - err(new WorkspaceSsoProviderNotFoundError()) - })(canQueryProjectArgs()) + getWorkspaceSsoProvider: async () => null + })(canReadProjectArgs()) await expect(result).resolves.toStrictEqual(ok()) }) it('does not allow project access if SSO session is missing', async () => { - const canQueryProject = canQueryProjectPolicy({ + const canReadProject = canReadProjectPolicy({ getEnv: async () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'true' @@ -434,21 +394,21 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { isPublic: false, workspaceId: crs({ length: 10 }) }), - getProjectRole: async () => ok('stream:contributor'), - getServerRole: async () => ok('server:user'), + getProjectRole: async () => Roles.Stream.Contributor, + getServerRole: async () => Roles.Server.User, getWorkspace, - getWorkspaceRole: async () => ok('workspace:member'), - getWorkspaceSsoSession: async () => err(new WorkspaceSsoSessionNotFoundError()), - getWorkspaceSsoProvider: async () => ok({ providerId: 'foo' }) + getWorkspaceRole: async () => Roles.Workspace.Member, + getWorkspaceSsoSession: async () => null, + getWorkspaceSsoProvider: async () => ({ providerId: 'foo' }) }) - const canQuery = await canQueryProject(canQueryProjectArgs()) + const canQuery = await canReadProject(canReadProjectArgs()) expect(canQuery.isOk).toBe(false) }) it('does not allow project access if SSO session is not found', async () => { const date = new Date() date.setDate(date.getDate() - 1) - const result = canQueryProjectPolicy({ + const result = canReadProjectPolicy({ getEnv: async () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'true' @@ -458,13 +418,13 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { isPublic: false, workspaceId: crs({ length: 10 }) }), - getProjectRole: async () => ok('stream:contributor'), - getServerRole: async () => ok('server:user'), - getWorkspaceRole: async () => ok('workspace:member'), + getProjectRole: async () => Roles.Stream.Contributor, + getServerRole: async () => Roles.Server.User, + getWorkspaceRole: async () => Roles.Workspace.Member, getWorkspace, - getWorkspaceSsoSession: async () => err(new WorkspaceSsoSessionNotFoundError()), - getWorkspaceSsoProvider: async () => ok({ providerId: 'foo' }) - })(canQueryProjectArgs()) + getWorkspaceSsoSession: async () => null, + getWorkspaceSsoProvider: async () => ({ providerId: 'foo' }) + })(canReadProjectArgs()) await expect(result).resolves.toStrictEqual( err( new WorkspaceSsoSessionNoAccessError({ @@ -477,7 +437,7 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { const date = new Date() date.setDate(date.getDate() - 1) - const result = canQueryProjectPolicy({ + const result = canReadProjectPolicy({ getEnv: async () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'true' @@ -487,14 +447,17 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { isPublic: false, workspaceId: crs({ length: 10 }) }), - getProjectRole: async () => ok('stream:contributor'), - getServerRole: async () => ok('server:user'), + getProjectRole: async () => Roles.Stream.Contributor, + getServerRole: async () => Roles.Server.User, getWorkspace, - getWorkspaceRole: async () => ok('workspace:member'), - getWorkspaceSsoSession: async () => - ok({ validUntil: date, userId: 'foo', providerId: 'foo' }), - getWorkspaceSsoProvider: async () => ok({ providerId: 'foo' }) - })(canQueryProjectArgs()) + getWorkspaceRole: async () => Roles.Workspace.Member, + getWorkspaceSsoSession: async () => ({ + validUntil: date, + userId: 'foo', + providerId: 'foo' + }), + getWorkspaceSsoProvider: async () => ({ providerId: 'foo' }) + })(canReadProjectArgs()) await expect(result).resolves.toStrictEqual( err( new WorkspaceSsoSessionNoAccessError({ @@ -507,7 +470,7 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { const date = new Date() date.setDate(date.getDate() + 1) - const result = canQueryProjectPolicy({ + const result = canReadProjectPolicy({ getEnv: async () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'true' @@ -517,14 +480,17 @@ describe('canQueryProjectPolicy creates a function, that handles ', () => { isPublic: false, workspaceId: crs({ length: 10 }) }), - getProjectRole: async () => ok('stream:contributor'), - getServerRole: async () => ok('server:user'), + getProjectRole: async () => Roles.Stream.Contributor, + getServerRole: async () => Roles.Server.User, getWorkspace, - getWorkspaceRole: async () => ok('workspace:member'), - getWorkspaceSsoSession: async () => - ok({ validUntil: date, userId: 'foo', providerId: 'foo' }), - getWorkspaceSsoProvider: async () => ok({ providerId: 'foo' }) - })(canQueryProjectArgs()) + getWorkspaceRole: async () => Roles.Workspace.Member, + getWorkspaceSsoSession: async () => ({ + validUntil: date, + userId: 'foo', + providerId: 'foo' + }), + getWorkspaceSsoProvider: async () => ({ providerId: 'foo' }) + })(canReadProjectArgs()) await expect(result).resolves.toStrictEqual(ok()) }) }) diff --git a/packages/shared/src/authz/policies/canQueryProject.ts b/packages/shared/src/authz/policies/canReadProject.ts similarity index 79% rename from packages/shared/src/authz/policies/canQueryProject.ts rename to packages/shared/src/authz/policies/canReadProject.ts index 23c9b2d65..41d39d0fc 100644 --- a/packages/shared/src/authz/policies/canQueryProject.ts +++ b/packages/shared/src/authz/policies/canReadProject.ts @@ -10,13 +10,13 @@ import { } from '../domain/authErrors.js' import { err, ok } from 'true-myth/result' import { AuthCheckContextLoaderKeys } from '../domain/loaders.js' -import { AuthPolicy, MaybeUserContext, ProjectContext } from '../domain/policies.js' +import { AuthPolicy } from '../domain/policies.js' import { canUseAdminOverride, hasMinimumServerRole } from '../checks/serverRole.js' import { hasAnyWorkspaceRole } from '../checks/workspaceRole.js' import { maybeMemberRoleWithValidSsoSessionIfNeeded } from '../fragments/workspaceSso.js' -import { throwUncoveredError } from '../../core/index.js' +import { MaybeUserContext, ProjectContext } from '../domain/context.js' -export const canQueryProjectPolicy: AuthPolicy< +export const canReadProjectPolicy: AuthPolicy< | typeof AuthCheckContextLoaderKeys.getEnv | typeof AuthCheckContextLoaderKeys.getProject | typeof AuthCheckContextLoaderKeys.getProjectRole @@ -37,22 +37,8 @@ export const canQueryProjectPolicy: AuthPolicy< async ({ userId, projectId }) => { const env = await loaders.getEnv() - // we prerolad the project and early return any loading errors - // this is a short circuit in the frontend, to surface any project load errors - // from the backend. - // make sure to expose all of the error types in the loader type, - // that we care about in this early return const project = await loaders.getProject({ projectId }) - if (project.isErr) { - switch (project.error.code) { - case 'ProjectNoAccess': - case 'ProjectNotFound': - case 'WorkspaceSsoSessionNoAccess': - return err(project.error) - default: - throwUncoveredError(project.error) - } - } + if (!project) return err(new ProjectNotFoundError()) // All users may read public projects if (await isPubliclyReadableProject(loaders)({ projectId })) return ok() @@ -68,7 +54,8 @@ export const canQueryProjectPolicy: AuthPolicy< // When G O D M O D E is enabled if (await canUseAdminOverride(loaders)({ userId })) return ok() - const { workspaceId } = project.value + // todo + const { workspaceId } = project // When a project belongs to a workspace if (env.FF_WORKSPACES_MODULE_ENABLED && !!workspaceId) { // User must have a workspace role to read project data diff --git a/packages/shared/src/authz/policies/index.ts b/packages/shared/src/authz/policies/index.ts index cc565ef27..c3c415b45 100644 --- a/packages/shared/src/authz/policies/index.ts +++ b/packages/shared/src/authz/policies/index.ts @@ -1,9 +1,13 @@ import { AllAuthCheckContextLoaders } from '../domain/loaders.js' -import { canQueryProjectPolicy } from './canQueryProject.js' +import { canCreateWorkspaceProjectPolicy } from './canCreateWorkspaceProject.js' +import { canReadProjectPolicy } from './canReadProject.js' export const authPoliciesFactory = (loaders: AllAuthCheckContextLoaders) => ({ project: { - canQuery: canQueryProjectPolicy(loaders) + canRead: canReadProjectPolicy(loaders) + }, + workspace: { + canCreateProject: canCreateWorkspaceProjectPolicy(loaders) } }) diff --git a/packages/shared/src/tests/fakes.ts b/packages/shared/src/tests/fakes.ts index 489d46dfd..26e5253d4 100644 --- a/packages/shared/src/tests/fakes.ts +++ b/packages/shared/src/tests/fakes.ts @@ -1,20 +1,19 @@ import { merge } from 'lodash' import { Project } from '../authz/domain/projects/types.js' -import { ok, Result } from 'true-myth/result' import { nanoid } from 'nanoid' export const fakeGetFactory = >(defaults: T) => (overrides?: Partial) => - (): Promise> => { + (): Promise => { if (overrides) { - return Promise.resolve(ok(merge(defaults, overrides))) + return Promise.resolve(merge(defaults, overrides)) } - return Promise.resolve(ok(defaults)) + return Promise.resolve(defaults) } export const getProjectFake = fakeGetFactory({ - id: nanoid(), + id: nanoid(10), isPublic: false, isDiscoverable: false, workspaceId: null diff --git a/packages/shared/src/workspaces/helpers/features.ts b/packages/shared/src/workspaces/helpers/features.ts index 9664ed913..f28666eae 100644 --- a/packages/shared/src/workspaces/helpers/features.ts +++ b/packages/shared/src/workspaces/helpers/features.ts @@ -1,4 +1,5 @@ import { WorkspaceRoles } from '../../core/constants.js' +import { WorkspaceLimits } from './limits.js' import { PaidWorkspacePlans, UnpaidWorkspacePlans, @@ -83,9 +84,15 @@ export type WorkspacePlanPriceStructure = { } } +const unlimited: WorkspaceLimits = { + projectCount: null, + modelCount: null +} + export type WorkspacePlanConfig = { plan: Plan features: readonly WorkspacePlanFeatures[] + limits: WorkspaceLimits } const baseFeatures = [ @@ -101,7 +108,8 @@ export const WorkspacePaidPlanConfigs: { // Old [PaidWorkspacePlans.Starter]: { plan: PaidWorkspacePlans.Starter, - features: [...baseFeatures, WorkspacePlanFeatures.DomainSecurity] + features: [...baseFeatures, WorkspacePlanFeatures.DomainSecurity], + limits: unlimited }, [PaidWorkspacePlans.Plus]: { plan: PaidWorkspacePlans.Plus, @@ -109,7 +117,8 @@ export const WorkspacePaidPlanConfigs: { ...baseFeatures, WorkspacePlanFeatures.DomainSecurity, WorkspacePlanFeatures.SSO - ] + ], + limits: unlimited }, [PaidWorkspacePlans.Business]: { plan: PaidWorkspacePlans.Business, @@ -119,11 +128,16 @@ export const WorkspacePaidPlanConfigs: { WorkspacePlanFeatures.SSO, WorkspacePlanFeatures.CustomDataRegion, WorkspacePlanFeatures.PrioritySupport - ] + ], + limits: unlimited }, [PaidWorkspacePlans.Team]: { plan: PaidWorkspacePlans.Team, - features: baseFeatures + features: baseFeatures, + limits: { + projectCount: 5, + modelCount: 25 + } }, [PaidWorkspacePlans.Pro]: { plan: PaidWorkspacePlans.Pro, @@ -133,7 +147,11 @@ export const WorkspacePaidPlanConfigs: { WorkspacePlanFeatures.SSO, WorkspacePlanFeatures.CustomDataRegion, WorkspacePlanFeatures.PrioritySupport - ] + ], + limits: { + projectCount: 10, + modelCount: 50 + } } } @@ -149,7 +167,8 @@ export const WorkspaceUnpaidPlanConfigs: { WorkspacePlanFeatures.SSO, WorkspacePlanFeatures.CustomDataRegion, WorkspacePlanFeatures.PrioritySupport - ] + ], + limits: unlimited }, [UnpaidWorkspacePlans.Academia]: { plan: UnpaidWorkspacePlans.Academia, @@ -159,7 +178,8 @@ export const WorkspaceUnpaidPlanConfigs: { WorkspacePlanFeatures.SSO, WorkspacePlanFeatures.CustomDataRegion, WorkspacePlanFeatures.PrioritySupport - ] + ], + limits: unlimited }, [UnpaidWorkspacePlans.StarterInvoiced]: { ...WorkspacePaidPlanConfigs.starter, @@ -176,7 +196,11 @@ export const WorkspaceUnpaidPlanConfigs: { // New [UnpaidWorkspacePlans.Free]: { plan: UnpaidWorkspacePlans.Free, - features: baseFeatures + features: baseFeatures, + limits: { + projectCount: 1, + modelCount: 5 + } } } diff --git a/packages/shared/src/workspaces/helpers/limits.ts b/packages/shared/src/workspaces/helpers/limits.ts new file mode 100644 index 000000000..da7bcc97f --- /dev/null +++ b/packages/shared/src/workspaces/helpers/limits.ts @@ -0,0 +1,4 @@ +export type WorkspaceLimits = { + projectCount: number | null + modelCount: number | null +} diff --git a/packages/shared/src/workspaces/helpers/plans.ts b/packages/shared/src/workspaces/helpers/plans.ts index 481f0c40f..79efa5cd6 100644 --- a/packages/shared/src/workspaces/helpers/plans.ts +++ b/packages/shared/src/workspaces/helpers/plans.ts @@ -1,3 +1,4 @@ +import { throwUncoveredError } from '../../core/helpers/error.js' import type { MaybeNullOrUndefined } from '../../core/helpers/utilityTypes.js' /** @@ -121,3 +122,39 @@ export const WorkspacePlanStatuses = { export type WorkspacePlanStatuses = (typeof WorkspacePlanStatuses)[keyof typeof WorkspacePlanStatuses] + +type BaseWorkspacePlan = { + workspaceId: string + createdAt: Date +} + +export type PaidWorkspacePlan = BaseWorkspacePlan & { + name: PaidWorkspacePlans + status: PaidWorkspacePlanStatuses +} + +export type TrialWorkspacePlan = BaseWorkspacePlan & { + name: TrialEnabledPaidWorkspacePlans + status: TrialWorkspacePlanStatuses +} + +export type UnpaidWorkspacePlan = BaseWorkspacePlan & { + name: UnpaidWorkspacePlans + status: UnpaidWorkspacePlanStatuses +} +export type WorkspacePlan = PaidWorkspacePlan | TrialWorkspacePlan | UnpaidWorkspacePlan + +export const isWorkspacePlanStatusReadOnly = (status: WorkspacePlan['status']) => { + switch (status) { + case 'cancelationScheduled': + case 'valid': + case 'trial': + case 'paymentFailed': + return false + case 'expired': + case 'canceled': + return true + default: + throwUncoveredError(status) + } +}