feat(authz): Project.canCreateModel and Project.canMoveToWorkspace policies (#4342)
* feat(authz): Project.canCreateModel policy * feat(authz): Project.canMoveToWorkspace policy * fix(authz): expose policies as permissions objects * chore(authz): actually use the policies lol * chore(authz): add tests for new policies * fix(authz): skip affected test * fix(authz): pr comments * fix(authz): better errors, better tests * chore(authz): remove references to deleted error
This commit is contained in:
@@ -3,6 +3,8 @@ extend type Project {
|
||||
}
|
||||
|
||||
type ProjectPermissionChecks {
|
||||
canCreateModel: PermissionCheckResult!
|
||||
canMoveToWorkspace(workspaceId: String!): PermissionCheckResult!
|
||||
canRead: PermissionCheckResult!
|
||||
}
|
||||
|
||||
|
||||
@@ -4,4 +4,5 @@ extend type Workspace {
|
||||
|
||||
type WorkspacePermissionChecks {
|
||||
canCreateProject: PermissionCheckResult!
|
||||
canMoveProjectToWorkspace(projectId: String!): PermissionCheckResult!
|
||||
}
|
||||
|
||||
@@ -2560,9 +2560,16 @@ export const ProjectPendingVersionsUpdatedMessageType = {
|
||||
export type ProjectPendingVersionsUpdatedMessageType = typeof ProjectPendingVersionsUpdatedMessageType[keyof typeof ProjectPendingVersionsUpdatedMessageType];
|
||||
export type ProjectPermissionChecks = {
|
||||
__typename?: 'ProjectPermissionChecks';
|
||||
canCreateModel: PermissionCheckResult;
|
||||
canMoveToWorkspace: PermissionCheckResult;
|
||||
canRead: PermissionCheckResult;
|
||||
};
|
||||
|
||||
|
||||
export type ProjectPermissionChecksCanMoveToWorkspaceArgs = {
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type ProjectRole = {
|
||||
__typename?: 'ProjectRole';
|
||||
project: Project;
|
||||
@@ -4764,6 +4771,12 @@ export type WorkspacePaymentMethod = typeof WorkspacePaymentMethod[keyof typeof
|
||||
export type WorkspacePermissionChecks = {
|
||||
__typename?: 'WorkspacePermissionChecks';
|
||||
canCreateProject: PermissionCheckResult;
|
||||
canMoveProjectToWorkspace: PermissionCheckResult;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspacePermissionChecksCanMoveProjectToWorkspaceArgs = {
|
||||
projectId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type WorkspacePlan = {
|
||||
@@ -6641,6 +6654,8 @@ export type ProjectPendingVersionsUpdatedMessageResolvers<ContextType = GraphQLC
|
||||
};
|
||||
|
||||
export type ProjectPermissionChecksResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['ProjectPermissionChecks'] = ResolversParentTypes['ProjectPermissionChecks']> = {
|
||||
canCreateModel?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canMoveToWorkspace?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType, RequireFields<ProjectPermissionChecksCanMoveToWorkspaceArgs, 'workspaceId'>>;
|
||||
canRead?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
@@ -7350,6 +7365,7 @@ export type WorkspaceMutationsResolvers<ContextType = GraphQLContext, ParentType
|
||||
|
||||
export type WorkspacePermissionChecksResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspacePermissionChecks'] = ResolversParentTypes['WorkspacePermissionChecks']> = {
|
||||
canCreateProject?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canMoveProjectToWorkspace?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType, RequireFields<WorkspacePermissionChecksCanMoveProjectToWorkspaceArgs, 'projectId'>>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
getRegisteredRegionClients
|
||||
} from '@/modules/multiregion/utils/dbSelector'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { mapAuthToServerError } from '@/modules/shared/helpers/errorHelper'
|
||||
|
||||
export = {
|
||||
User: {
|
||||
@@ -296,12 +297,15 @@ export = {
|
||||
},
|
||||
ModelMutations: {
|
||||
async create(_parent, args, ctx) {
|
||||
await authorizeResolver(
|
||||
ctx.userId,
|
||||
args.input.projectId,
|
||||
Roles.Stream.Contributor,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
const canCreate = await ctx.authPolicies.project.canCreateModel({
|
||||
userId: ctx.userId,
|
||||
projectId: args.input.projectId
|
||||
})
|
||||
|
||||
if (!canCreate.isOk) {
|
||||
throw mapAuthToServerError(canCreate.error)
|
||||
}
|
||||
|
||||
const projectDB = await getProjectDbClient({ projectId: args.input.projectId })
|
||||
|
||||
// Sanitize model name by trimming spaces around slashes
|
||||
|
||||
@@ -9,6 +9,21 @@ export default {
|
||||
permissions: () => ({})
|
||||
},
|
||||
ProjectPermissionChecks: {
|
||||
canCreateModel: async (parent, _args, ctx) => {
|
||||
const canCreateModel = await ctx.authPolicies.project.canCreateModel({
|
||||
userId: ctx.userId,
|
||||
projectId: parent.projectId
|
||||
})
|
||||
return Authz.toGraphqlResult(canCreateModel)
|
||||
},
|
||||
canMoveToWorkspace: async (parent, args, ctx) => {
|
||||
const canMoveToWorkspace = await ctx.authPolicies.project.canMoveToWorkspace({
|
||||
userId: ctx.userId,
|
||||
projectId: parent.projectId,
|
||||
workspaceId: args.workspaceId
|
||||
})
|
||||
return Authz.toGraphqlResult(canMoveToWorkspace)
|
||||
},
|
||||
canRead: async (parent, _args, ctx) => {
|
||||
const canRead = await ctx.authPolicies.project.canRead({
|
||||
projectId: parent.projectId,
|
||||
|
||||
@@ -2540,9 +2540,16 @@ export const ProjectPendingVersionsUpdatedMessageType = {
|
||||
export type ProjectPendingVersionsUpdatedMessageType = typeof ProjectPendingVersionsUpdatedMessageType[keyof typeof ProjectPendingVersionsUpdatedMessageType];
|
||||
export type ProjectPermissionChecks = {
|
||||
__typename?: 'ProjectPermissionChecks';
|
||||
canCreateModel: PermissionCheckResult;
|
||||
canMoveToWorkspace: PermissionCheckResult;
|
||||
canRead: PermissionCheckResult;
|
||||
};
|
||||
|
||||
|
||||
export type ProjectPermissionChecksCanMoveToWorkspaceArgs = {
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type ProjectRole = {
|
||||
__typename?: 'ProjectRole';
|
||||
project: Project;
|
||||
@@ -4744,6 +4751,12 @@ export type WorkspacePaymentMethod = typeof WorkspacePaymentMethod[keyof typeof
|
||||
export type WorkspacePermissionChecks = {
|
||||
__typename?: 'WorkspacePermissionChecks';
|
||||
canCreateProject: PermissionCheckResult;
|
||||
canMoveProjectToWorkspace: PermissionCheckResult;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspacePermissionChecksCanMoveProjectToWorkspaceArgs = {
|
||||
projectId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type WorkspacePlan = {
|
||||
|
||||
@@ -65,6 +65,7 @@ import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { getTotalSeatsCountByPlanFactory } from '@/modules/gatekeeper/services/subscriptions'
|
||||
import { queryAllWorkspaceProjectsFactory } from '@/modules/workspaces/services/projects'
|
||||
import { legacyGetStreamsFactory } from '@/modules/core/repositories/streams'
|
||||
import { getWorkspaceModelCountFactory } from '@/modules/workspaces/services/workspaceLimits'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import { getPaginatedProjectModelsTotalCountFactory } from '@/modules/core/repositories/branches'
|
||||
|
||||
@@ -184,29 +185,18 @@ export = FF_GATEKEEPER_MODULE_ENABLED
|
||||
modelCount: async (parent) => {
|
||||
const { workspaceId } = parent
|
||||
|
||||
let modelCount = 0
|
||||
|
||||
const queryAllWorkspaceProjects = queryAllWorkspaceProjectsFactory({
|
||||
getStreams: legacyGetStreamsFactory({ db })
|
||||
})
|
||||
|
||||
for await (const projects of queryAllWorkspaceProjects({ workspaceId })) {
|
||||
for (const project of projects) {
|
||||
const regionDb = await getProjectDbClient({ projectId: project.id })
|
||||
const projectModelCount =
|
||||
await getPaginatedProjectModelsTotalCountFactory({ db: regionDb })(
|
||||
project.id,
|
||||
{
|
||||
filter: {
|
||||
onlyWithVersions: true
|
||||
}
|
||||
}
|
||||
)
|
||||
modelCount = modelCount + projectModelCount
|
||||
return await getWorkspaceModelCountFactory({
|
||||
queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({
|
||||
getStreams: legacyGetStreamsFactory({ db })
|
||||
}),
|
||||
getPaginatedProjectModelsTotalCount: async (projectId, params) => {
|
||||
const regionDb = await getProjectDbClient({ projectId })
|
||||
return await getPaginatedProjectModelsTotalCountFactory({ db: regionDb })(
|
||||
projectId,
|
||||
params
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return modelCount
|
||||
})({ workspaceId })
|
||||
}
|
||||
},
|
||||
WorkspaceSubscription: {
|
||||
|
||||
@@ -33,6 +33,7 @@ export const mapAuthToServerError = (e: Authz.AllAuthErrors): BaseError => {
|
||||
case Authz.WorkspaceReadOnlyError.code:
|
||||
case Authz.WorkspaceLimitsReachedError.code:
|
||||
case Authz.WorkspaceNoEditorSeatError.code:
|
||||
case Authz.WorkspaceProjectMoveInvalidError.code:
|
||||
return new ForbiddenError(e.message)
|
||||
case Authz.WorkspaceSsoSessionNoAccessError.code:
|
||||
throw new SsoSessionMissingOrExpiredError(e.message, {
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { db } from '@/db/knex'
|
||||
import { getPaginatedProjectModelsTotalCountFactory } from '@/modules/core/repositories/branches'
|
||||
import { legacyGetStreamsFactory } from '@/modules/core/repositories/streams'
|
||||
import { getWorkspacePlanFactory } from '@/modules/gatekeeper/repositories/billing'
|
||||
import { defineModuleLoaders } from '@/modules/loaders'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import {
|
||||
getUserSsoSessionFactory,
|
||||
getWorkspaceSsoProviderRecordFactory
|
||||
} from '@/modules/workspaces/repositories/sso'
|
||||
import { getWorkspaceRoleForUserFactory } from '@/modules/workspaces/repositories/workspaces'
|
||||
import { queryAllWorkspaceProjectsFactory } from '@/modules/workspaces/services/projects'
|
||||
import { getWorkspaceModelCountFactory } from '@/modules/workspaces/services/workspaceLimits'
|
||||
import { WorkspacePaidPlanConfigs, WorkspaceUnpaidPlanConfigs } from '@speckle/shared'
|
||||
|
||||
// TODO: Move everything to use dataLoaders
|
||||
@@ -46,6 +51,21 @@ export default defineModuleLoaders(async () => {
|
||||
)?.type || null
|
||||
)
|
||||
},
|
||||
getWorkspaceModelCount: async ({ workspaceId }) => {
|
||||
// TODO: Dataloader that has to dynamically pick regional dbs?
|
||||
return await getWorkspaceModelCountFactory({
|
||||
queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({
|
||||
getStreams: legacyGetStreamsFactory({ db })
|
||||
}),
|
||||
getPaginatedProjectModelsTotalCount: async (projectId, params) => {
|
||||
const regionDb = await getProjectDbClient({ projectId })
|
||||
return await getPaginatedProjectModelsTotalCountFactory({ db: regionDb })(
|
||||
projectId,
|
||||
params
|
||||
)
|
||||
}
|
||||
})({ workspaceId })
|
||||
},
|
||||
getWorkspaceProjectCount: async ({ workspaceId }, { dataLoaders }) => {
|
||||
return await dataLoaders.workspaces!.getProjectCount.load(workspaceId)
|
||||
},
|
||||
|
||||
@@ -241,6 +241,10 @@ export type GetWorkspacesProjectsCounts = (params: {
|
||||
[workspaceId: string]: number
|
||||
}>
|
||||
|
||||
export type GetWorkspaceModelCount = (params: {
|
||||
workspaceId: string
|
||||
}) => Promise<number>
|
||||
|
||||
export type GetPaginatedWorkspaceProjectsArgs = {
|
||||
workspaceId: string
|
||||
/**
|
||||
|
||||
@@ -14,6 +14,15 @@ export default {
|
||||
userId: ctx.userId
|
||||
})
|
||||
return Authz.toGraphqlResult(canCreateProject)
|
||||
},
|
||||
canMoveProjectToWorkspace: async (parent, args, ctx) => {
|
||||
const canMoveProjectToWorkspace =
|
||||
await ctx.authPolicies.project.canMoveToWorkspace({
|
||||
userId: ctx.userId,
|
||||
projectId: args.projectId,
|
||||
workspaceId: parent.workspaceId
|
||||
})
|
||||
return Authz.toGraphqlResult(canMoveProjectToWorkspace)
|
||||
}
|
||||
}
|
||||
} as Resolvers
|
||||
|
||||
@@ -193,7 +193,6 @@ import { getProjectFactory } from '@/modules/core/repositories/projects'
|
||||
import { getProjectRegionKey } from '@/modules/multiregion/utils/regionSelector'
|
||||
import { scheduleJob } from '@/modules/multiregion/services/queue'
|
||||
import { updateWorkspacePlanFactory } from '@/modules/gatekeeper/services/workspacePlans'
|
||||
import { OperationTypeNode } from 'graphql'
|
||||
import { GetWorkspaceCollaboratorsArgs } from '@/modules/workspaces/domain/operations'
|
||||
import { WorkspaceTeamMember } from '@/modules/workspaces/domain/types'
|
||||
import { UsersMeta } from '@/modules/core/dbSchema'
|
||||
@@ -209,6 +208,7 @@ import {
|
||||
getWorkspaceRolesAndSeatsFactory,
|
||||
getWorkspaceUserSeatFactory
|
||||
} from '@/modules/gatekeeper/repositories/workspaceSeat'
|
||||
import { mapAuthToServerError } from '@/modules/shared/helpers/errorHelper'
|
||||
|
||||
const eventBus = getEventBus()
|
||||
const getServerInfo = getServerInfoFactory({ db })
|
||||
@@ -1057,20 +1057,16 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
moveToWorkspace: async (_parent, args, context) => {
|
||||
const { projectId, workspaceId } = args
|
||||
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
projectId,
|
||||
Roles.Stream.Owner,
|
||||
context.resourceAccessRules,
|
||||
OperationTypeNode.MUTATION
|
||||
)
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
workspaceId,
|
||||
Roles.Workspace.Admin,
|
||||
context.resourceAccessRules,
|
||||
OperationTypeNode.MUTATION
|
||||
)
|
||||
const canMoveToWorkspace =
|
||||
await context.authPolicies.project.canMoveToWorkspace({
|
||||
userId: context.userId,
|
||||
projectId,
|
||||
workspaceId
|
||||
})
|
||||
|
||||
if (!canMoveToWorkspace.isOk) {
|
||||
throw mapAuthToServerError(canMoveToWorkspace.error)
|
||||
}
|
||||
|
||||
const moveProjectToWorkspace = commandFactory({
|
||||
db,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { GetPaginatedProjectModelsTotalCount } from '@/modules/core/domain/branches/operations'
|
||||
import {
|
||||
GetWorkspaceModelCount,
|
||||
QueryAllWorkspaceProjects
|
||||
} from '@/modules/workspaces/domain/operations'
|
||||
|
||||
// TODO: Optimize with single model count query per regional db
|
||||
export const getWorkspaceModelCountFactory =
|
||||
(deps: {
|
||||
queryAllWorkspaceProjects: QueryAllWorkspaceProjects
|
||||
getPaginatedProjectModelsTotalCount: GetPaginatedProjectModelsTotalCount
|
||||
}): GetWorkspaceModelCount =>
|
||||
async ({ workspaceId }) => {
|
||||
let modelCount = 0
|
||||
|
||||
for await (const projects of deps.queryAllWorkspaceProjects({ workspaceId })) {
|
||||
for (const project of projects) {
|
||||
modelCount =
|
||||
modelCount +
|
||||
(await deps.getPaginatedProjectModelsTotalCount(project.id, {
|
||||
filter: {
|
||||
onlyWithVersions: true
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
return modelCount
|
||||
}
|
||||
@@ -17,6 +17,9 @@ export default defineModuleLoaders(() => ({
|
||||
getWorkspaceSeat: async () => {
|
||||
throw new LoaderUnsupportedError()
|
||||
},
|
||||
getWorkspaceModelCount: async () => {
|
||||
throw new LoaderUnsupportedError()
|
||||
},
|
||||
getWorkspaceProjectCount: async () => {
|
||||
throw new LoaderUnsupportedError()
|
||||
},
|
||||
|
||||
@@ -8,3 +8,9 @@ export class SsoSessionMissingOrExpiredError extends BaseError<{
|
||||
static code = 'SSO_SESSION_MISSING_OR_EXPIRED_ERROR'
|
||||
static statusCode = 401
|
||||
}
|
||||
|
||||
export class WorkspaceRequiredError extends BaseError {
|
||||
static defaultMessage = 'This action requires a workspace.'
|
||||
static code = 'WORKSPACE_REQUIRED_ERROR'
|
||||
static statusCode = 400
|
||||
}
|
||||
|
||||
@@ -2541,9 +2541,16 @@ export const ProjectPendingVersionsUpdatedMessageType = {
|
||||
export type ProjectPendingVersionsUpdatedMessageType = typeof ProjectPendingVersionsUpdatedMessageType[keyof typeof ProjectPendingVersionsUpdatedMessageType];
|
||||
export type ProjectPermissionChecks = {
|
||||
__typename?: 'ProjectPermissionChecks';
|
||||
canCreateModel: PermissionCheckResult;
|
||||
canMoveToWorkspace: PermissionCheckResult;
|
||||
canRead: PermissionCheckResult;
|
||||
};
|
||||
|
||||
|
||||
export type ProjectPermissionChecksCanMoveToWorkspaceArgs = {
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type ProjectRole = {
|
||||
__typename?: 'ProjectRole';
|
||||
project: Project;
|
||||
@@ -4745,6 +4752,12 @@ export type WorkspacePaymentMethod = typeof WorkspacePaymentMethod[keyof typeof
|
||||
export type WorkspacePermissionChecks = {
|
||||
__typename?: 'WorkspacePermissionChecks';
|
||||
canCreateProject: PermissionCheckResult;
|
||||
canMoveProjectToWorkspace: PermissionCheckResult;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspacePermissionChecksCanMoveProjectToWorkspaceArgs = {
|
||||
projectId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type WorkspacePlan = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { hasAnyWorkspaceRole, requireMinimumWorkspaceRole } from './workspaceRole.js'
|
||||
import { hasAnyWorkspaceRole, hasMinimumWorkspaceRole } from './workspaceRole.js'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
|
||||
describe('hasAnyWorkspaceRole returns a function, that', () => {
|
||||
@@ -25,7 +25,7 @@ describe('hasAnyWorkspaceRole returns a function, that', () => {
|
||||
|
||||
describe('requireMinimumWorkspaceRole returns a function, that', () => {
|
||||
it('turns non existing workspace role into false ', async () => {
|
||||
const result = await requireMinimumWorkspaceRole({
|
||||
const result = await hasMinimumWorkspaceRole({
|
||||
getWorkspaceRole: async () => null
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
@@ -35,7 +35,7 @@ describe('requireMinimumWorkspaceRole returns a function, that', () => {
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
it('returns false if user is below target role', async () => {
|
||||
const result = await requireMinimumWorkspaceRole({
|
||||
const result = await hasMinimumWorkspaceRole({
|
||||
getWorkspaceRole: async () => 'workspace:member'
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
@@ -45,7 +45,7 @@ describe('requireMinimumWorkspaceRole returns a function, that', () => {
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
it('returns true if user matches target role', async () => {
|
||||
const result = await requireMinimumWorkspaceRole({
|
||||
const result = await hasMinimumWorkspaceRole({
|
||||
getWorkspaceRole: async () => 'workspace:member'
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
@@ -55,7 +55,7 @@ describe('requireMinimumWorkspaceRole returns a function, that', () => {
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
it('returns true if user exceeds target role', async () => {
|
||||
const result = await requireMinimumWorkspaceRole({
|
||||
const result = await hasMinimumWorkspaceRole({
|
||||
getWorkspaceRole: async () => 'workspace:admin'
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
|
||||
@@ -3,7 +3,7 @@ import { UserContext, WorkspaceContext } from '../domain/context.js'
|
||||
import { isMinimumWorkspaceRole } from '../domain/logic/roles.js'
|
||||
import { AuthPolicyCheck } from '../domain/policies.js'
|
||||
|
||||
export const requireMinimumWorkspaceRole: AuthPolicyCheck<
|
||||
export const hasMinimumWorkspaceRole: AuthPolicyCheck<
|
||||
'getWorkspaceRole',
|
||||
UserContext & WorkspaceContext & { role: WorkspaceRoles }
|
||||
> =
|
||||
|
||||
@@ -90,6 +90,11 @@ export const WorkspaceLimitsReachedError = defineAuthError<
|
||||
message: 'Workspace limits have been reached'
|
||||
})
|
||||
|
||||
export const WorkspaceProjectMoveInvalidError = defineAuthError({
|
||||
code: 'WorkspaceProjectMoveInvalid',
|
||||
message: 'Projects already in a workspace cannot be moved to another workspace.'
|
||||
})
|
||||
|
||||
export const WorkspaceSsoSessionNoAccessError = defineAuthError<
|
||||
'WorkspaceSsoSessionNoAccess',
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
GetEnv,
|
||||
GetWorkspace,
|
||||
GetWorkspaceLimits,
|
||||
GetWorkspaceModelCount,
|
||||
GetWorkspacePlan,
|
||||
GetWorkspaceProjectCount,
|
||||
GetWorkspaceRole,
|
||||
@@ -51,6 +52,7 @@ export const AuthCheckContextLoaderKeys = <const>{
|
||||
getWorkspace: 'getWorkspace',
|
||||
getWorkspaceRole: 'getWorkspaceRole',
|
||||
getWorkspaceSeat: 'getWorkspaceSeat',
|
||||
getWorkspaceModelCount: 'getWorkspaceModelCount',
|
||||
getWorkspaceProjectCount: 'getWorkspaceProjectCount',
|
||||
getWorkspacePlan: 'getWorkspacePlan',
|
||||
getWorkspaceLimits: 'getWorkspaceLimits',
|
||||
@@ -76,6 +78,7 @@ export type AllAuthCheckContextLoaders = AuthContextLoaderMappingDefinition<{
|
||||
getWorkspacePlan: GetWorkspacePlan
|
||||
getWorkspaceSeat: GetWorkspaceSeat
|
||||
getWorkspaceProjectCount: GetWorkspaceProjectCount
|
||||
getWorkspaceModelCount: GetWorkspaceModelCount
|
||||
getWorkspaceSsoProvider: GetWorkspaceSsoProvider
|
||||
getWorkspaceSsoSession: GetWorkspaceSsoSession
|
||||
}>
|
||||
|
||||
@@ -21,6 +21,8 @@ export type GetWorkspaceProjectCount = (
|
||||
args: WorkspaceContext
|
||||
) => Promise<number | null>
|
||||
|
||||
export type GetWorkspaceModelCount = (args: WorkspaceContext) => Promise<number | null>
|
||||
|
||||
export type GetWorkspaceSeat = (
|
||||
args: UserContext & WorkspaceContext
|
||||
) => Promise<WorkspaceSeatType | null>
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { AuthPolicyEnsureFragment } from '../domain/policies.js'
|
||||
import {
|
||||
hasAnyWorkspaceRole,
|
||||
requireMinimumWorkspaceRole
|
||||
} from '../checks/workspaceRole.js'
|
||||
import { hasMinimumWorkspaceRole } from '../checks/workspaceRole.js'
|
||||
import {
|
||||
WorkspaceNoAccessError,
|
||||
WorkspacesNotEnabledError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../domain/authErrors.js'
|
||||
import { Loaders } from '../domain/loaders.js'
|
||||
import { Roles, WorkspaceRoles } from '../../core/constants.js'
|
||||
|
||||
/**
|
||||
* Ensure user has a workspace role, and a valid SSO session (if SSO is configured)
|
||||
@@ -19,20 +17,24 @@ export const ensureWorkspaceRoleAndSessionFragment: AuthPolicyEnsureFragment<
|
||||
| 'getWorkspaceSsoProvider'
|
||||
| 'getWorkspaceSsoSession'
|
||||
| 'getWorkspace',
|
||||
{ userId: string; workspaceId: string },
|
||||
{ userId: string; workspaceId: string; role?: WorkspaceRoles },
|
||||
InstanceType<typeof WorkspaceSsoSessionNoAccessError | typeof WorkspaceNoAccessError>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, workspaceId }) => {
|
||||
async ({ userId, workspaceId, role }) => {
|
||||
// Get workspace, so we can resolve its slug for error scenarios
|
||||
const workspace = await loaders.getWorkspace({ workspaceId })
|
||||
// hides the fact, that the workspace does not exist
|
||||
if (!workspace) return err(new WorkspaceNoAccessError())
|
||||
|
||||
const hasAnyRole = await hasAnyWorkspaceRole(loaders)({ userId, workspaceId })
|
||||
if (!hasAnyRole) return err(new WorkspaceNoAccessError())
|
||||
const hasMinimumRole = await hasMinimumWorkspaceRole(loaders)({
|
||||
userId,
|
||||
workspaceId,
|
||||
role: role ?? Roles.Workspace.Guest
|
||||
})
|
||||
if (!hasMinimumRole) return err(new WorkspaceNoAccessError())
|
||||
|
||||
const hasMinimumMemberRole = await requireMinimumWorkspaceRole(loaders)({
|
||||
const hasMinimumMemberRole = await hasMinimumWorkspaceRole(loaders)({
|
||||
userId,
|
||||
workspaceId,
|
||||
role: 'workspace:member'
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { AllAuthCheckContextLoaders } from '../domain/loaders.js'
|
||||
import { canCreateWorkspaceProjectPolicy } from './workspace/canCreateWorkspaceProject.js'
|
||||
import { canReadProjectPolicy } from './project/canReadProject.js'
|
||||
import { canCreateModelPolicy } from './project/canCreateModel.js'
|
||||
import { canMoveToWorkspacePolicy } from './project/canMoveToWorkspace.js'
|
||||
import { canCreatePersonalProjectPolicy } from './project/canCreatePersonal.js'
|
||||
|
||||
export const authPoliciesFactory = (loaders: AllAuthCheckContextLoaders) => ({
|
||||
project: {
|
||||
canRead: canReadProjectPolicy(loaders),
|
||||
canCreateModel: canCreateModelPolicy(loaders),
|
||||
canMoveToWorkspace: canMoveToWorkspacePolicy(loaders),
|
||||
canCreatePersonal: canCreatePersonalProjectPolicy(loaders)
|
||||
},
|
||||
workspace: {
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { assert, describe, expect, it } from 'vitest'
|
||||
import { canCreateModelPolicy } from './canCreateModel.js'
|
||||
import { parseFeatureFlags } from '../../../environment/index.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import { Workspace } from '../../domain/workspaces/types.js'
|
||||
import { WorkspacePlan } from '../../../workspaces/index.js'
|
||||
import { Project } from '../../domain/projects/types.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceLimitsReachedError,
|
||||
WorkspaceNoAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
|
||||
const buildCanCreateModelPolicy = (
|
||||
overrides?: Partial<Parameters<typeof canCreateModelPolicy>[0]>
|
||||
) =>
|
||||
canCreateModelPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: async () => {
|
||||
return {
|
||||
id: cryptoRandomString({ length: 9 }),
|
||||
isPublic: false,
|
||||
isDiscoverable: false,
|
||||
workspaceId: cryptoRandomString({ length: 9 })
|
||||
}
|
||||
},
|
||||
getProjectRole: async () => {
|
||||
return Roles.Stream.Contributor
|
||||
},
|
||||
getServerRole: async () => {
|
||||
return Roles.Server.User
|
||||
},
|
||||
getWorkspace: async () => {
|
||||
return {} as Workspace
|
||||
},
|
||||
getWorkspaceRole: async () => {
|
||||
return Roles.Workspace.Guest
|
||||
},
|
||||
getWorkspaceSsoProvider: async () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoSession: async () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspacePlan: async () => {
|
||||
return {
|
||||
status: 'valid'
|
||||
} as WorkspacePlan
|
||||
},
|
||||
getWorkspaceLimits: async () => {
|
||||
return {
|
||||
modelCount: 5,
|
||||
projectCount: 1,
|
||||
versionsHistory: null
|
||||
}
|
||||
},
|
||||
getWorkspaceModelCount: async () => {
|
||||
return 0
|
||||
},
|
||||
...overrides
|
||||
})
|
||||
|
||||
const canCreateArgs = () => ({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
projectId: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
|
||||
describe('canCreateModelPolicy returns a function, that', () => {
|
||||
it('forbids unauthenticated users', async () => {
|
||||
const result = await buildCanCreateModelPolicy({})({
|
||||
userId: undefined,
|
||||
projectId: ''
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoSessionError.code
|
||||
})
|
||||
})
|
||||
it('forbids users without server roles', async () => {
|
||||
const result = await buildCanCreateModelPolicy({
|
||||
getServerRole: async () => {
|
||||
return null
|
||||
}
|
||||
})(canCreateArgs())
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoAccessError.code
|
||||
})
|
||||
})
|
||||
it('forbids users that are not at least stream contributors', async () => {
|
||||
const result = await buildCanCreateModelPolicy({
|
||||
getProjectRole: async () => {
|
||||
return Roles.Stream.Reviewer
|
||||
}
|
||||
})(canCreateArgs())
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
it('allows stream contributors to create personal projects when project is not in a workspace', async () => {
|
||||
const result = await buildCanCreateModelPolicy({
|
||||
getProject: async () => {
|
||||
return {} as Project
|
||||
}
|
||||
})(canCreateArgs())
|
||||
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
// Hold the workspace to a higher standard than myself
|
||||
it('requires the workspace to have a plan', async () => {
|
||||
const result = await buildCanCreateModelPolicy({
|
||||
getWorkspacePlan: async () => {
|
||||
return null
|
||||
}
|
||||
})(canCreateArgs())
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceNoAccessError.code
|
||||
})
|
||||
})
|
||||
it('forbids new model creation if workspace has reached limit', async () => {
|
||||
const result = await buildCanCreateModelPolicy({
|
||||
getWorkspaceLimits: async () => {
|
||||
return {
|
||||
projectCount: 1,
|
||||
modelCount: 5,
|
||||
versionsHistory: null
|
||||
}
|
||||
},
|
||||
getWorkspaceModelCount: async () => {
|
||||
return 5
|
||||
}
|
||||
})(canCreateArgs())
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceLimitsReachedError.code,
|
||||
payload: { limit: 'modelCount' }
|
||||
})
|
||||
})
|
||||
it('allows new model creation if workspace is within limits', async () => {
|
||||
const result = await buildCanCreateModelPolicy({})(canCreateArgs())
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,102 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import {
|
||||
ProjectNotFoundError,
|
||||
ProjectNoAccessError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionNoAccessError,
|
||||
WorkspaceLimitsReachedError,
|
||||
ServerNoSessionError,
|
||||
ServerNoAccessError,
|
||||
WorkspaceReadOnlyError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { MaybeUserContext, ProjectContext } from '../../domain/context.js'
|
||||
import { AuthCheckContextLoaderKeys } from '../../domain/loaders.js'
|
||||
import { AuthPolicy } from '../../domain/policies.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import { isWorkspacePlanStatusReadOnly } from '../../../workspaces/index.js'
|
||||
import { ensureMinimumServerRoleFragment } from '../../fragments/server.js'
|
||||
import {
|
||||
ensureMinimumProjectRoleFragment,
|
||||
ensureProjectWorkspaceAccessFragment
|
||||
} from '../../fragments/projects.js'
|
||||
|
||||
type PolicyLoaderKeys =
|
||||
| typeof AuthCheckContextLoaderKeys.getEnv
|
||||
| typeof AuthCheckContextLoaderKeys.getProject
|
||||
| typeof AuthCheckContextLoaderKeys.getProjectRole
|
||||
| typeof AuthCheckContextLoaderKeys.getServerRole
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspace
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceRole
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceSsoProvider
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceSsoSession
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspacePlan
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceLimits
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceModelCount
|
||||
|
||||
type PolicyArgs = MaybeUserContext & ProjectContext
|
||||
|
||||
type PolicyErrors =
|
||||
| InstanceType<typeof ProjectNotFoundError>
|
||||
| InstanceType<typeof ProjectNoAccessError>
|
||||
| InstanceType<typeof WorkspaceNoAccessError>
|
||||
| InstanceType<typeof WorkspaceSsoSessionNoAccessError>
|
||||
| InstanceType<typeof WorkspaceReadOnlyError>
|
||||
| InstanceType<typeof WorkspaceLimitsReachedError>
|
||||
| InstanceType<typeof ServerNoSessionError>
|
||||
| InstanceType<typeof ServerNoAccessError>
|
||||
|
||||
export const canCreateModelPolicy: AuthPolicy<
|
||||
PolicyLoaderKeys,
|
||||
PolicyArgs,
|
||||
PolicyErrors
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId }) => {
|
||||
const ensuredServerRole = await ensureMinimumServerRoleFragment(loaders)({
|
||||
userId,
|
||||
role: Roles.Server.Guest
|
||||
})
|
||||
if (ensuredServerRole.isErr) return err(ensuredServerRole.error)
|
||||
|
||||
const ensuredProjectRole = await ensureMinimumProjectRoleFragment(loaders)({
|
||||
userId: userId!,
|
||||
projectId,
|
||||
role: Roles.Stream.Contributor
|
||||
})
|
||||
if (ensuredProjectRole.isErr) return err(ensuredProjectRole.error)
|
||||
|
||||
const project = await loaders.getProject({ projectId })
|
||||
|
||||
// Projects outside of a workspace do not need to check workspace limits
|
||||
if (!project?.workspaceId) {
|
||||
return ok()
|
||||
}
|
||||
|
||||
const { workspaceId } = project
|
||||
|
||||
const ensuredWorkspaceAccess = await ensureProjectWorkspaceAccessFragment(loaders)({
|
||||
userId: userId!,
|
||||
projectId
|
||||
})
|
||||
if (ensuredWorkspaceAccess.isErr) {
|
||||
return err(ensuredWorkspaceAccess.error)
|
||||
}
|
||||
|
||||
const workspacePlan = await loaders.getWorkspacePlan({ workspaceId })
|
||||
if (!workspacePlan) return err(new WorkspaceNoAccessError())
|
||||
if (isWorkspacePlanStatusReadOnly(workspacePlan.status))
|
||||
return err(new WorkspaceReadOnlyError())
|
||||
|
||||
const workspaceLimits = await loaders.getWorkspaceLimits({ workspaceId })
|
||||
if (!workspaceLimits) return err(new WorkspaceNoAccessError())
|
||||
|
||||
if (workspaceLimits.modelCount === null) return ok()
|
||||
|
||||
const currentModelCount = await loaders.getWorkspaceModelCount({ workspaceId })
|
||||
|
||||
if (currentModelCount === null) return err(new WorkspaceNoAccessError())
|
||||
|
||||
return currentModelCount < workspaceLimits.modelCount
|
||||
? ok()
|
||||
: err(new WorkspaceLimitsReachedError({ payload: { limit: 'modelCount' } }))
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { assert, describe, expect, it } from 'vitest'
|
||||
import { canMoveToWorkspacePolicy } from './canMoveToWorkspace.js'
|
||||
import { parseFeatureFlags } from '../../../environment/index.js'
|
||||
import { Project } from '../../domain/projects/types.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import { Workspace } from '../../domain/workspaces/types.js'
|
||||
import { WorkspacePlan } from '../../../workspaces/index.js'
|
||||
|
||||
const buildCanMoveToWorkspace = (
|
||||
overrides?: Partial<Parameters<typeof canMoveToWorkspacePolicy>[0]>
|
||||
) =>
|
||||
canMoveToWorkspacePolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: async () => {
|
||||
return {} as Project
|
||||
},
|
||||
getProjectRole: async () => {
|
||||
return Roles.Stream.Owner
|
||||
},
|
||||
getServerRole: async () => {
|
||||
return Roles.Server.User
|
||||
},
|
||||
getWorkspace: async () => {
|
||||
return {} as Workspace
|
||||
},
|
||||
getWorkspaceRole: async () => {
|
||||
return Roles.Workspace.Admin
|
||||
},
|
||||
getWorkspaceSsoProvider: async () => {
|
||||
return null
|
||||
},
|
||||
getWorkspaceSsoSession: async () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspacePlan: async () => {
|
||||
return {
|
||||
status: 'valid'
|
||||
} as WorkspacePlan
|
||||
},
|
||||
getWorkspaceLimits: async () => {
|
||||
return {
|
||||
modelCount: 5,
|
||||
projectCount: 5,
|
||||
versionsHistory: null
|
||||
}
|
||||
},
|
||||
getWorkspaceProjectCount: async () => {
|
||||
return 0
|
||||
},
|
||||
...overrides
|
||||
})
|
||||
|
||||
const canMoveToWorkspaceArgs = () => ({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
projectId: cryptoRandomString({ length: 9 }),
|
||||
workspaceId: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
|
||||
describe('canMoveToWorkspacePolicy returns a function, that', () => {
|
||||
it('requires workspaces to be enabled', async () => {
|
||||
const result = await buildCanMoveToWorkspace({
|
||||
getEnv: async () =>
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'false'
|
||||
})
|
||||
})(canMoveToWorkspaceArgs())
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: 'WorkspacesNotEnabled'
|
||||
})
|
||||
})
|
||||
it('requires the project to not be in a workspace', async () => {
|
||||
const result = await buildCanMoveToWorkspace({
|
||||
getProject: async () => {
|
||||
return {
|
||||
workspaceId: cryptoRandomString({ length: 9 })
|
||||
} as Project
|
||||
}
|
||||
})(canMoveToWorkspaceArgs())
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: 'WorkspaceProjectMoveInvalid'
|
||||
})
|
||||
})
|
||||
it('requires user to be a server user', async () => {
|
||||
const result = await buildCanMoveToWorkspace({
|
||||
getServerRole: async () => {
|
||||
return Roles.Server.Guest
|
||||
}
|
||||
})(canMoveToWorkspaceArgs())
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: 'ServerNoAccess'
|
||||
})
|
||||
})
|
||||
it('requires user to be project owner', async () => {
|
||||
const result = await buildCanMoveToWorkspace({
|
||||
getProjectRole: async () => {
|
||||
return Roles.Stream.Contributor
|
||||
}
|
||||
})(canMoveToWorkspaceArgs())
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: 'ProjectNoAccess'
|
||||
})
|
||||
})
|
||||
it('requires user to be target workspace admin', async () => {
|
||||
const result = await buildCanMoveToWorkspace({
|
||||
getWorkspaceRole: async () => {
|
||||
return Roles.Workspace.Member
|
||||
}
|
||||
})(canMoveToWorkspaceArgs())
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: 'WorkspaceNoAccess'
|
||||
})
|
||||
})
|
||||
it('forbids move if target workspace will exceed plan limits', async () => {
|
||||
const result = await buildCanMoveToWorkspace({
|
||||
getWorkspaceLimits: async () => {
|
||||
return {
|
||||
projectCount: 1,
|
||||
modelCount: 5,
|
||||
versionsHistory: null
|
||||
}
|
||||
},
|
||||
getWorkspaceProjectCount: async () => {
|
||||
return 1
|
||||
}
|
||||
})(canMoveToWorkspaceArgs())
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: 'WorkspaceLimitsReached',
|
||||
payload: { limit: 'projectCount' }
|
||||
})
|
||||
})
|
||||
it('allows move project if target workspace will be within limits', async () => {
|
||||
const result = await buildCanMoveToWorkspace({})(canMoveToWorkspaceArgs())
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,111 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceLimitsReachedError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceProjectMoveInvalidError,
|
||||
WorkspaceReadOnlyError,
|
||||
WorkspacesNotEnabledError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
import {
|
||||
MaybeUserContext,
|
||||
ProjectContext,
|
||||
WorkspaceContext
|
||||
} from '../../domain/context.js'
|
||||
import { AuthCheckContextLoaderKeys } from '../../domain/loaders.js'
|
||||
import { AuthPolicy } from '../../domain/policies.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import { isWorkspacePlanStatusReadOnly } from '../../../workspaces/index.js'
|
||||
import {
|
||||
ensureWorkspaceRoleAndSessionFragment,
|
||||
ensureWorkspacesEnabledFragment
|
||||
} from '../../fragments/workspaces.js'
|
||||
import { ensureMinimumServerRoleFragment } from '../../fragments/server.js'
|
||||
import { ensureMinimumProjectRoleFragment } from '../../fragments/projects.js'
|
||||
|
||||
type PolicyLoaderKeys =
|
||||
| typeof AuthCheckContextLoaderKeys.getEnv
|
||||
| typeof AuthCheckContextLoaderKeys.getProject
|
||||
| typeof AuthCheckContextLoaderKeys.getProjectRole
|
||||
| typeof AuthCheckContextLoaderKeys.getServerRole
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspace
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceRole
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceSsoProvider
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceSsoSession
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspacePlan
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceLimits
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceProjectCount
|
||||
|
||||
type PolicyArgs = MaybeUserContext & ProjectContext & WorkspaceContext
|
||||
|
||||
type PolicyErrors =
|
||||
| InstanceType<typeof ProjectNotFoundError>
|
||||
| InstanceType<typeof ProjectNoAccessError>
|
||||
| InstanceType<typeof WorkspaceNoAccessError>
|
||||
| InstanceType<typeof WorkspaceSsoSessionNoAccessError>
|
||||
| InstanceType<typeof WorkspaceReadOnlyError>
|
||||
| InstanceType<typeof WorkspaceLimitsReachedError>
|
||||
| InstanceType<typeof WorkspacesNotEnabledError>
|
||||
| InstanceType<typeof WorkspaceProjectMoveInvalidError>
|
||||
| InstanceType<typeof ServerNoSessionError>
|
||||
| InstanceType<typeof ServerNoAccessError>
|
||||
|
||||
export const canMoveToWorkspacePolicy: AuthPolicy<
|
||||
PolicyLoaderKeys,
|
||||
PolicyArgs,
|
||||
PolicyErrors
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId, workspaceId }) => {
|
||||
const ensuredWorkspacesEnabled = await ensureWorkspacesEnabledFragment(loaders)({})
|
||||
if (ensuredWorkspacesEnabled.isErr) return err(ensuredWorkspacesEnabled.error)
|
||||
|
||||
// We do not support moving projects that are already in a workspace
|
||||
const project = await loaders.getProject({ projectId })
|
||||
if (!project) return err(new ProjectNotFoundError())
|
||||
if (!!project.workspaceId) return err(new WorkspaceProjectMoveInvalidError())
|
||||
|
||||
const ensuredServerRole = await ensureMinimumServerRoleFragment(loaders)({
|
||||
userId,
|
||||
role: Roles.Server.User
|
||||
})
|
||||
if (ensuredServerRole.isErr) return err(ensuredServerRole.error)
|
||||
|
||||
const ensuredProjectRole = await ensureMinimumProjectRoleFragment(loaders)({
|
||||
userId: userId!,
|
||||
projectId,
|
||||
role: Roles.Stream.Owner
|
||||
})
|
||||
if (ensuredProjectRole.isErr) return err(ensuredProjectRole.error)
|
||||
|
||||
const ensuredWorkspaceAccess = await ensureWorkspaceRoleAndSessionFragment(loaders)(
|
||||
{
|
||||
userId: userId!,
|
||||
workspaceId,
|
||||
role: Roles.Workspace.Admin
|
||||
}
|
||||
)
|
||||
if (ensuredWorkspaceAccess.isErr) return err(ensuredWorkspaceAccess.error)
|
||||
|
||||
const workspacePlan = await loaders.getWorkspacePlan({ workspaceId })
|
||||
if (!workspacePlan) return err(new WorkspaceNoAccessError())
|
||||
if (isWorkspacePlanStatusReadOnly(workspacePlan.status))
|
||||
return err(new WorkspaceReadOnlyError())
|
||||
|
||||
const workspaceLimits = await loaders.getWorkspaceLimits({ workspaceId })
|
||||
if (!workspaceLimits) return err(new WorkspaceNoAccessError())
|
||||
|
||||
if (workspaceLimits.projectCount === null) return ok()
|
||||
|
||||
const currentProjectCount = await loaders.getWorkspaceProjectCount({ workspaceId })
|
||||
|
||||
if (currentProjectCount === null) return err(new WorkspaceNoAccessError())
|
||||
|
||||
return currentProjectCount < workspaceLimits.projectCount
|
||||
? ok()
|
||||
: err(new WorkspaceLimitsReachedError({ payload: { limit: 'projectCount' } }))
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
isWorkspacePlanStatusReadOnly
|
||||
} from '../../../workspaces/index.js'
|
||||
import { ensureMinimumServerRoleFragment } from '../../fragments/server.js'
|
||||
import { requireMinimumWorkspaceRole } from '../../checks/workspaceRole.js'
|
||||
import { hasMinimumWorkspaceRole } from '../../checks/workspaceRole.js'
|
||||
|
||||
export const canCreateWorkspaceProjectPolicy: AuthPolicy<
|
||||
| 'getEnv'
|
||||
@@ -69,7 +69,7 @@ export const canCreateWorkspaceProjectPolicy: AuthPolicy<
|
||||
}
|
||||
|
||||
// guests cannot create projects in the workspace
|
||||
const isNotGuest = await requireMinimumWorkspaceRole(loaders)({
|
||||
const isNotGuest = await hasMinimumWorkspaceRole(loaders)({
|
||||
userId: userId!,
|
||||
workspaceId,
|
||||
role: Roles.Workspace.Member
|
||||
|
||||
Reference in New Issue
Block a user