Merge branch 'main' of github.com:specklesystems/speckle-server into alessandro/web-2944-versions-limits
This commit is contained in:
@@ -127,6 +127,26 @@ export const ServerNoSessionError = defineAuthError({
|
||||
message: 'You are not logged in to this server'
|
||||
})
|
||||
|
||||
export const CommentNotFoundError = defineAuthError({
|
||||
code: 'CommentNotFound',
|
||||
message: 'Comment not found'
|
||||
})
|
||||
|
||||
export const CommentNoAccessError = defineAuthError({
|
||||
code: 'CommentNoAccess',
|
||||
message: 'You do not have access to this comment'
|
||||
})
|
||||
|
||||
export const ModelNotFoundError = defineAuthError({
|
||||
code: 'ModelNotFound',
|
||||
message: 'Model not found'
|
||||
})
|
||||
|
||||
export const ReservedModelNotDeletableError = defineAuthError({
|
||||
code: 'ReservedModelNotDeletable',
|
||||
message: 'This model is reserved and cannot be deleted'
|
||||
})
|
||||
|
||||
// Resolve all exported error types
|
||||
export type AllAuthErrors = ValueOf<{
|
||||
[key in keyof typeof import('./authErrors.js')]: typeof import('./authErrors.js')[key] extends new (
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Comment } from './types.js'
|
||||
|
||||
export type GetComment = (args: {
|
||||
commentId: string
|
||||
projectId: string
|
||||
}) => Promise<Comment | null>
|
||||
@@ -0,0 +1,5 @@
|
||||
export type Comment = {
|
||||
id: string
|
||||
authorId: string
|
||||
projectId: string
|
||||
}
|
||||
@@ -1,4 +1,12 @@
|
||||
export type ProjectContext = { projectId: string }
|
||||
export type MaybeProjectContext = { projectId?: string }
|
||||
|
||||
export type UserContext = { userId: string }
|
||||
export type MaybeUserContext = { userId?: string }
|
||||
|
||||
export type WorkspaceContext = { workspaceId: string }
|
||||
export type MaybeWorkspaceContext = { workspaceId?: string }
|
||||
|
||||
export type CommentContext = { commentId: string }
|
||||
|
||||
export type ModelContext = { modelId: string }
|
||||
|
||||
@@ -19,6 +19,8 @@ import type {
|
||||
GetWorkspaceSsoProvider,
|
||||
GetWorkspaceSsoSession
|
||||
} from './workspaces/operations.js'
|
||||
import { GetComment } from './comments/operations.js'
|
||||
import { GetModel } from './models/operations.js'
|
||||
|
||||
// utility type that ensures all properties functions that return promises
|
||||
type PromiseAll<T> = {
|
||||
@@ -63,7 +65,9 @@ export const AuthCheckContextLoaderKeys = <const>{
|
||||
getWorkspaceLimits: 'getWorkspaceLimits',
|
||||
getWorkspaceSsoProvider: 'getWorkspaceSsoProvider',
|
||||
getWorkspaceSsoSession: 'getWorkspaceSsoSession',
|
||||
getAdminOverrideEnabled: 'getAdminOverrideEnabled'
|
||||
getAdminOverrideEnabled: 'getAdminOverrideEnabled',
|
||||
getComment: 'getComment',
|
||||
getModel: 'getModel'
|
||||
}
|
||||
export const Loaders = AuthCheckContextLoaderKeys // shorter alias
|
||||
/* v8 ignore end */
|
||||
@@ -87,6 +91,8 @@ export type AllAuthCheckContextLoaders = AuthContextLoaderMappingDefinition<{
|
||||
getWorkspaceModelCount: GetWorkspaceModelCount
|
||||
getWorkspaceSsoProvider: GetWorkspaceSsoProvider
|
||||
getWorkspaceSsoSession: GetWorkspaceSsoSession
|
||||
getComment: GetComment
|
||||
getModel: GetModel
|
||||
}>
|
||||
|
||||
export type AuthCheckContextLoaders<
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Model } from './types.js'
|
||||
|
||||
export type GetModel = (args: {
|
||||
projectId: string
|
||||
modelId: string
|
||||
}) => Promise<Model | null>
|
||||
@@ -0,0 +1,6 @@
|
||||
export type Model = {
|
||||
id: string
|
||||
projectId: string
|
||||
name: string
|
||||
authorId: string | null
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export type Project = {
|
||||
isDiscoverable: boolean
|
||||
isPublic: boolean
|
||||
workspaceId: string | null
|
||||
allowPublicComments: boolean
|
||||
}
|
||||
|
||||
export type ProjectVisibility = 'public' | 'linkShareable' | 'private'
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
checkIfPubliclyReadableProjectFragment,
|
||||
ensureImplicitProjectMemberWithReadAccessFragment,
|
||||
ensureImplicitProjectMemberWithWriteAccessFragment,
|
||||
ensureMinimumProjectRoleFragment,
|
||||
ensureProjectWorkspaceAccessFragment
|
||||
} from './projects.js'
|
||||
@@ -16,11 +17,12 @@ import {
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../domain/authErrors.js'
|
||||
import { OverridesOf } from '../../tests/helpers/types.js'
|
||||
import { getProjectFake } from '../../tests/fakes.js'
|
||||
|
||||
describe('ensureMinimumProjectRoleFragment', () => {
|
||||
const buildSUT = (overrides?: OverridesOf<typeof ensureMinimumProjectRoleFragment>) =>
|
||||
ensureMinimumProjectRoleFragment({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -40,7 +42,7 @@ describe('ensureMinimumProjectRoleFragment', () => {
|
||||
overrides?: OverridesOf<typeof ensureMinimumProjectRoleFragment>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: 'workspaceId',
|
||||
isDiscoverable: false,
|
||||
@@ -160,7 +162,7 @@ describe('checkIfPubliclyReadableProjectFragment', () => {
|
||||
overrides?: OverridesOf<typeof checkIfPubliclyReadableProjectFragment>
|
||||
) =>
|
||||
checkIfPubliclyReadableProjectFragment({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -192,7 +194,7 @@ describe('checkIfPubliclyReadableProjectFragment', () => {
|
||||
|
||||
it('returns true if project is public', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -209,7 +211,7 @@ describe('checkIfPubliclyReadableProjectFragment', () => {
|
||||
|
||||
it('returns false if project is not public', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -228,7 +230,7 @@ describe('ensureProjectWorkspaceAccessFragment', () => {
|
||||
overrides?: OverridesOf<typeof ensureProjectWorkspaceAccessFragment>
|
||||
) =>
|
||||
ensureProjectWorkspaceAccessFragment({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -246,7 +248,7 @@ describe('ensureProjectWorkspaceAccessFragment', () => {
|
||||
overrides?: OverridesOf<typeof ensureProjectWorkspaceAccessFragment>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: 'workspaceId',
|
||||
isDiscoverable: false,
|
||||
@@ -384,7 +386,7 @@ describe('ensureImplicitProjectMemberWithReadAccessFragment', async () => {
|
||||
overrides?: OverridesOf<typeof ensureImplicitProjectMemberWithReadAccessFragment>
|
||||
) =>
|
||||
ensureImplicitProjectMemberWithReadAccessFragment({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -405,7 +407,7 @@ describe('ensureImplicitProjectMemberWithReadAccessFragment', async () => {
|
||||
overrides?: OverridesOf<typeof ensureImplicitProjectMemberWithReadAccessFragment>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: 'workspaceId',
|
||||
isDiscoverable: false,
|
||||
@@ -599,3 +601,275 @@ describe('ensureImplicitProjectMemberWithReadAccessFragment', async () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ensureImplicitProjectMemberWithWriteAccessFragment', () => {
|
||||
const buildSUT = (
|
||||
overrides?: OverridesOf<typeof ensureImplicitProjectMemberWithWriteAccessFragment>
|
||||
) =>
|
||||
ensureImplicitProjectMemberWithWriteAccessFragment({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
}),
|
||||
getServerRole: async () => Roles.Server.User,
|
||||
getProjectRole: async () => Roles.Stream.Contributor,
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getWorkspace: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
getWorkspaceRole: async () => null,
|
||||
...overrides
|
||||
})
|
||||
|
||||
const buildWorkspaceSUT = (
|
||||
overrides?: OverridesOf<typeof ensureImplicitProjectMemberWithWriteAccessFragment>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: 'workspaceId',
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
}),
|
||||
getProjectRole: async () => null,
|
||||
getWorkspace: async () => ({
|
||||
id: 'workspaceId',
|
||||
slug: 'workspaceSlug'
|
||||
}),
|
||||
getWorkspaceSsoProvider: async () => ({
|
||||
providerId: 'ssoProviderId'
|
||||
}),
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
providerId: 'ssoSessionId',
|
||||
userId: 'userId',
|
||||
validUntil: new Date(Date.now() + 1000 * 60 * 60 * 24)
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Admin,
|
||||
...overrides
|
||||
})
|
||||
|
||||
it('succeeds with explicit member role', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('fails if user not specified', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
projectId: 'projectId'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoSessionError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getServerRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user is guest and asking for owner', async () => {
|
||||
const sut = buildSUT({
|
||||
getServerRole: async () => Roles.Server.Guest
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId',
|
||||
role: Roles.Stream.Owner
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails w/o role even if admin', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => null,
|
||||
getServerRole: async () => Roles.Server.Admin
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId',
|
||||
role: Roles.Stream.Reviewer
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails without project role', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('succeeds with reviewer role, if permitted', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => Roles.Stream.Reviewer
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId',
|
||||
role: Roles.Stream.Reviewer
|
||||
})
|
||||
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('fails with a too restrictive project role', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => Roles.Stream.Reviewer
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
describe('with workspace project', () => {
|
||||
it('succeeds with implicit project role', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('fails if workspace role not permissive enough', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('succeeds w/ low workspace role if allowed', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId',
|
||||
role: Roles.Stream.Reviewer
|
||||
})
|
||||
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('succeeds w/o sso session, if workspace guest w/ explicit project role', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceRole: async () => Roles.Workspace.Guest,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
getProjectRole: async () => Roles.Stream.Contributor
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('succeeds w/o sso session, if not configured', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('fails if no sso session, but required', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if sso session expired', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
providerId: 'ssoSessionId',
|
||||
userId: 'userId',
|
||||
validUntil: new Date(Date.now() - 1000 * 60 * 60 * 24)
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -187,15 +187,6 @@ export const ensureImplicitProjectMemberWithReadAccessFragment: AuthPolicyEnsure
|
||||
}
|
||||
if (isAdminOverrideEnabled.value) return ok()
|
||||
|
||||
// No god mode, ensure workspace access
|
||||
const ensuredWorkspaceAccess = await ensureProjectWorkspaceAccessFragment(loaders)({
|
||||
userId: userId!,
|
||||
projectId
|
||||
})
|
||||
if (ensuredWorkspaceAccess.isErr) {
|
||||
return err(ensuredWorkspaceAccess.error)
|
||||
}
|
||||
|
||||
// And ensure (implicit/explicit) project role
|
||||
const ensuredProjectRole = await ensureMinimumProjectRoleFragment(loaders)({
|
||||
userId: userId!,
|
||||
@@ -206,5 +197,82 @@ export const ensureImplicitProjectMemberWithReadAccessFragment: AuthPolicyEnsure
|
||||
return err(ensuredProjectRole.error)
|
||||
}
|
||||
|
||||
// No god mode, ensure workspace access
|
||||
const ensuredWorkspaceAccess = await ensureProjectWorkspaceAccessFragment(loaders)({
|
||||
userId: userId!,
|
||||
projectId
|
||||
})
|
||||
if (ensuredWorkspaceAccess.isErr) {
|
||||
return err(ensuredWorkspaceAccess.error)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure user has implicit/explicit project membership and write access
|
||||
*/
|
||||
export const ensureImplicitProjectMemberWithWriteAccessFragment: AuthPolicyEnsureFragment<
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole,
|
||||
MaybeUserContext &
|
||||
ProjectContext & {
|
||||
/**
|
||||
* By default assumes Contributor+ for any writes, but some operations
|
||||
* may allow for lower roles (e.g. comments)
|
||||
*/
|
||||
role?: StreamRoles
|
||||
},
|
||||
InstanceType<
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof WorkspaceSsoSessionNoAccessError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId, role }) => {
|
||||
const requiredProjectRole = role || Roles.Stream.Contributor
|
||||
const requiredServerRole =
|
||||
requiredProjectRole === Roles.Stream.Owner
|
||||
? Roles.Server.User
|
||||
: Roles.Server.Guest
|
||||
|
||||
// Ensure user is authed
|
||||
const ensuredServerRole = await ensureMinimumServerRoleFragment(loaders)({
|
||||
userId,
|
||||
role: requiredServerRole
|
||||
})
|
||||
if (ensuredServerRole.isErr) {
|
||||
return err(ensuredServerRole.error)
|
||||
}
|
||||
|
||||
// And ensure (implicit/explicit) project role
|
||||
const ensuredProjectRole = await ensureMinimumProjectRoleFragment(loaders)({
|
||||
userId: userId!,
|
||||
projectId,
|
||||
role: requiredProjectRole
|
||||
})
|
||||
if (ensuredProjectRole.isErr) {
|
||||
return err(ensuredProjectRole.error)
|
||||
}
|
||||
|
||||
// Ensure workspace access
|
||||
const ensuredWorkspaceAccess = await ensureProjectWorkspaceAccessFragment(loaders)({
|
||||
userId: userId!,
|
||||
projectId
|
||||
})
|
||||
if (ensuredWorkspaceAccess.isErr) {
|
||||
return err(ensuredWorkspaceAccess.error)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
|
||||
@@ -1,13 +1,31 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { AuthPolicyEnsureFragment } from '../domain/policies.js'
|
||||
import { hasMinimumWorkspaceRole } from '../checks/workspaceRole.js'
|
||||
import {
|
||||
hasAnyWorkspaceRole,
|
||||
hasMinimumWorkspaceRole
|
||||
} from '../checks/workspaceRole.js'
|
||||
import {
|
||||
ProjectNotFoundError,
|
||||
WorkspaceLimitsReachedError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceNoEditorSeatError,
|
||||
WorkspaceNotEnoughPermissionsError,
|
||||
WorkspaceReadOnlyError,
|
||||
WorkspacesNotEnabledError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../domain/authErrors.js'
|
||||
import { Loaders } from '../domain/loaders.js'
|
||||
import { Roles, WorkspaceRoles } from '../../core/constants.js'
|
||||
import {
|
||||
MaybeUserContext,
|
||||
ProjectContext,
|
||||
WorkspaceContext
|
||||
} from '../domain/context.js'
|
||||
import {
|
||||
isNewWorkspacePlan,
|
||||
isWorkspacePlanStatusReadOnly
|
||||
} from '../../workspaces/helpers/plans.js'
|
||||
import { hasEditorSeat } from '../checks/workspaceSeat.js'
|
||||
|
||||
/**
|
||||
* Ensure user has a workspace role, and a valid SSO session (if SSO is configured)
|
||||
@@ -84,3 +102,154 @@ export const ensureWorkspacesEnabledFragment: AuthPolicyEnsureFragment<
|
||||
if (!env.FF_WORKSPACES_MODULE_ENABLED) return err(new WorkspacesNotEnabledError())
|
||||
return ok()
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure workspace is not read-only
|
||||
*/
|
||||
export const ensureWorkspaceNotReadOnlyFragment: AuthPolicyEnsureFragment<
|
||||
typeof Loaders.getWorkspacePlan,
|
||||
WorkspaceContext,
|
||||
InstanceType<typeof WorkspaceNoAccessError | typeof WorkspaceReadOnlyError>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ workspaceId }) => {
|
||||
const workspacePlan = await loaders.getWorkspacePlan({ workspaceId })
|
||||
if (!workspacePlan) return err(new WorkspaceNoAccessError())
|
||||
if (isWorkspacePlanStatusReadOnly(workspacePlan.status))
|
||||
return err(new WorkspaceReadOnlyError())
|
||||
|
||||
return ok()
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure workspace can accept new project (not read-only, limits not reached).
|
||||
* If userId is specified, will also check for user role & seat
|
||||
*/
|
||||
export const ensureWorkspaceProjectCanBeCreatedFragment: AuthPolicyEnsureFragment<
|
||||
| typeof Loaders.getWorkspacePlan
|
||||
| typeof Loaders.getWorkspaceSeat
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspaceLimits
|
||||
| typeof Loaders.getWorkspaceProjectCount,
|
||||
WorkspaceContext & MaybeUserContext,
|
||||
InstanceType<
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof WorkspaceReadOnlyError
|
||||
| typeof WorkspaceLimitsReachedError
|
||||
| typeof WorkspaceNoEditorSeatError
|
||||
| typeof WorkspaceNotEnoughPermissionsError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ workspaceId, userId }) => {
|
||||
// First check user even has access
|
||||
if (userId) {
|
||||
// Is Member+
|
||||
const isNotGuest = await hasMinimumWorkspaceRole(loaders)({
|
||||
userId,
|
||||
workspaceId,
|
||||
role: Roles.Workspace.Member
|
||||
})
|
||||
if (!isNotGuest) {
|
||||
return err(
|
||||
new WorkspaceNotEnoughPermissionsError(
|
||||
'Guests cannot create projects in the workspace'
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const ensuredNotReadOnly = await ensureWorkspaceNotReadOnlyFragment(loaders)({
|
||||
workspaceId
|
||||
})
|
||||
if (ensuredNotReadOnly.isErr) return err(ensuredNotReadOnly.error)
|
||||
|
||||
const workspacePlan = await loaders.getWorkspacePlan({ workspaceId })
|
||||
if (!workspacePlan) return err(new WorkspaceNoAccessError())
|
||||
|
||||
// Now check editor seat
|
||||
if (userId) {
|
||||
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' } }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure model can be created (workspace not read-only, limits not reached).
|
||||
* If userId is specified, will also check for appropriate user role & seat
|
||||
*/
|
||||
export const ensureModelCanBeCreatedFragment: AuthPolicyEnsureFragment<
|
||||
| typeof Loaders.getWorkspacePlan
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspaceLimits
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getWorkspaceModelCount,
|
||||
ProjectContext & MaybeUserContext,
|
||||
InstanceType<
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof WorkspaceReadOnlyError
|
||||
| typeof WorkspaceLimitsReachedError
|
||||
| typeof ProjectNotFoundError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ projectId, userId }) => {
|
||||
const project = await loaders.getProject({ projectId })
|
||||
if (!project) return err(new ProjectNotFoundError())
|
||||
|
||||
const { workspaceId } = project
|
||||
if (!workspaceId) return ok()
|
||||
|
||||
if (userId) {
|
||||
// Has workspace role
|
||||
const isInWorkspace = await hasAnyWorkspaceRole(loaders)({
|
||||
userId,
|
||||
workspaceId
|
||||
})
|
||||
if (!isInWorkspace) {
|
||||
return err(new WorkspaceNoAccessError())
|
||||
}
|
||||
}
|
||||
|
||||
const ensuredNotReadOnly = await ensureWorkspaceNotReadOnlyFragment(loaders)({
|
||||
workspaceId
|
||||
})
|
||||
if (ensuredNotReadOnly.isErr) return err(ensuredNotReadOnly.error)
|
||||
|
||||
const workspacePlan = await loaders.getWorkspacePlan({ workspaceId })
|
||||
if (!workspacePlan) return err(new WorkspaceNoAccessError())
|
||||
|
||||
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' } }))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { canCreateModelPolicy } from './project/model/canCreate.js'
|
||||
import { canMoveToWorkspacePolicy } from './project/canMoveToWorkspace.js'
|
||||
import { canCreatePersonalProjectPolicy } from './project/canCreatePersonal.js'
|
||||
import { canUpdateProjectPolicy } from './project/canUpdate.js'
|
||||
@@ -9,11 +9,27 @@ import { canReadProjectSettingsPolicy } from './project/canReadSettings.js'
|
||||
import { canReadProjectWebhooksPolicy } from './project/canReadWebhooks.js'
|
||||
import { canUpdateProjectAllowPublicCommentsPolicy } from './project/canUpdateAllowPublicComments.js'
|
||||
import { canLeaveProjectPolicy } from './project/canLeave.js'
|
||||
import { canBroadcastProjectActivityPolicy } from './project/canBroadcastActivity.js'
|
||||
import { canCreateProjectCommentPolicy } from './project/comment/canCreate.js'
|
||||
import { canArchiveProjectCommentPolicy } from './project/comment/canArchive.js'
|
||||
import { canEditProjectCommentPolicy } from './project/comment/canEdit.js'
|
||||
import { canUpdateModelPolicy } from './project/model/canUpdate.js'
|
||||
import { canDeleteModelPolicy } from './project/model/canDelete.js'
|
||||
|
||||
export const authPoliciesFactory = (loaders: AllAuthCheckContextLoaders) => ({
|
||||
project: {
|
||||
model: {
|
||||
canCreate: canCreateModelPolicy(loaders),
|
||||
canUpdate: canUpdateModelPolicy(loaders),
|
||||
canDelete: canDeleteModelPolicy(loaders)
|
||||
},
|
||||
comment: {
|
||||
canCreate: canCreateProjectCommentPolicy(loaders),
|
||||
canArchive: canArchiveProjectCommentPolicy(loaders),
|
||||
canEdit: canEditProjectCommentPolicy(loaders)
|
||||
},
|
||||
canBroadcastActivity: canBroadcastProjectActivityPolicy(loaders),
|
||||
canRead: canReadProjectPolicy(loaders),
|
||||
canCreateModel: canCreateModelPolicy(loaders),
|
||||
canMoveToWorkspace: canMoveToWorkspacePolicy(loaders),
|
||||
canCreatePersonal: canCreatePersonalProjectPolicy(loaders),
|
||||
canUpdate: canUpdateProjectPolicy(loaders),
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { OverridesOf } from '../../../tests/helpers/types.js'
|
||||
import { canBroadcastProjectActivityPolicy } from './canBroadcastActivity.js'
|
||||
import { parseFeatureFlags } from '../../../environment/index.js'
|
||||
import { getProjectFake } from '../../../tests/fakes.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
|
||||
describe('canBroadcastProjectActivityPolicy', () => {
|
||||
const buildSUT = (
|
||||
overrides?: OverridesOf<typeof canBroadcastProjectActivityPolicy>
|
||||
) =>
|
||||
canBroadcastProjectActivityPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
}),
|
||||
getAdminOverrideEnabled: async () => false,
|
||||
getProjectRole: async () => Roles.Stream.Reviewer,
|
||||
getServerRole: async () => Roles.Server.User,
|
||||
getWorkspace: async () => null,
|
||||
getWorkspaceRole: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
...overrides
|
||||
})
|
||||
|
||||
const buildWorkspaceSUT = (
|
||||
overrides?: OverridesOf<typeof canBroadcastProjectActivityPolicy>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
}),
|
||||
getProjectRole: async () => null,
|
||||
getWorkspace: async () => ({
|
||||
id: 'workspace-id',
|
||||
slug: 'workspace-slug'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getWorkspaceSsoProvider: async () => ({
|
||||
providerId: 'provider-id'
|
||||
}),
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date()
|
||||
}),
|
||||
...overrides
|
||||
})
|
||||
|
||||
it('succeeds w/ project role', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('succeeds w/o project role if public', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: true
|
||||
}),
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails if user undefined', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: undefined,
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoSessionError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getServerRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if project not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNotFoundError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user has no project role', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('succeeds w/ admin override, even w/o project role', async () => {
|
||||
const sut = buildSUT({
|
||||
getAdminOverrideEnabled: async () => true,
|
||||
getServerRole: async () => Roles.Server.Admin,
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
describe('with workspace project', async () => {
|
||||
it('succeeds w/ workspace role', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('succeeds w/o project & workspace role if public', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceRole: async () => null,
|
||||
getProjectRole: async () => null,
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
isPublic: true
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails if user has no workspace role', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('succeeds w/o sso, if not needed', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails w/o sso, if needed', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if sso expired', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date(Date.now() - 1000)
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { MaybeUserContext, ProjectContext } from '../../domain/context.js'
|
||||
import { Loaders } from '../../domain/loaders.js'
|
||||
import { AuthPolicy } from '../../domain/policies.js'
|
||||
import {
|
||||
checkIfPubliclyReadableProjectFragment,
|
||||
ensureImplicitProjectMemberWithReadAccessFragment
|
||||
} from '../../fragments/projects.js'
|
||||
import { ensureMinimumServerRoleFragment } from '../../fragments/server.js'
|
||||
|
||||
export const canBroadcastProjectActivityPolicy: AuthPolicy<
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole
|
||||
| typeof Loaders.getAdminOverrideEnabled,
|
||||
MaybeUserContext & ProjectContext,
|
||||
InstanceType<
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof WorkspaceSsoSessionNoAccessError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId }) => {
|
||||
// Ensure logged in
|
||||
const ensuredServerRole = await ensureMinimumServerRoleFragment(loaders)({
|
||||
userId
|
||||
})
|
||||
if (ensuredServerRole.isErr) {
|
||||
return err(ensuredServerRole.error)
|
||||
}
|
||||
|
||||
// If publicly readable - any authed user can broadcast
|
||||
const isPubliclyReadable = await checkIfPubliclyReadableProjectFragment(loaders)({
|
||||
projectId
|
||||
})
|
||||
if (isPubliclyReadable.isErr) {
|
||||
return err(isPubliclyReadable.error)
|
||||
}
|
||||
if (isPubliclyReadable.value) return ok()
|
||||
|
||||
// Not public. Ensure user has at least implicit membership & read access
|
||||
const hasReadAccess = await ensureImplicitProjectMemberWithReadAccessFragment(
|
||||
loaders
|
||||
)({
|
||||
userId,
|
||||
projectId
|
||||
})
|
||||
if (hasReadAccess.isErr) {
|
||||
return err(hasReadAccess.error)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
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' } }))
|
||||
}
|
||||
@@ -10,12 +10,13 @@ import {
|
||||
ServerNoSessionError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { getProjectFake } from '../../../tests/fakes.js'
|
||||
|
||||
describe('canLeaveProjectPolicy', () => {
|
||||
const buildSUT = (overrides?: OverridesOf<typeof canLeaveProjectPolicy>) =>
|
||||
canLeaveProjectPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -33,7 +34,7 @@ describe('canLeaveProjectPolicy', () => {
|
||||
|
||||
const buildWorkspaceSUT = (overrides?: OverridesOf<typeof canLeaveProjectPolicy>) =>
|
||||
buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { Roles, SeatTypes } from '../../../core/constants.js'
|
||||
import { Workspace } from '../../domain/workspaces/types.js'
|
||||
import { WorkspacePlan } from '../../../workspaces/index.js'
|
||||
|
||||
@@ -27,6 +27,7 @@ const buildCanMoveToWorkspace = (
|
||||
getWorkspaceRole: async () => {
|
||||
return Roles.Workspace.Admin
|
||||
},
|
||||
getWorkspaceSeat: async () => SeatTypes.Editor,
|
||||
getWorkspaceSsoProvider: async () => {
|
||||
return null
|
||||
},
|
||||
@@ -139,4 +140,32 @@ describe('canMoveToWorkspacePolicy returns a function, that', () => {
|
||||
const result = await buildCanMoveToWorkspace({})(canMoveToWorkspaceArgs())
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
it('allows validation without providing a project id', async () => {
|
||||
const result = await buildCanMoveToWorkspace({
|
||||
getProject: async () => {
|
||||
assert.fail()
|
||||
},
|
||||
getProjectRole: async () => {
|
||||
return null
|
||||
}
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
workspaceId: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
it('allows validation without providing a workspace id', async () => {
|
||||
const result = await buildCanMoveToWorkspace({
|
||||
getWorkspace: async () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: async () => {
|
||||
return null
|
||||
}
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
projectId: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,21 +6,23 @@ import {
|
||||
ServerNoSessionError,
|
||||
WorkspaceLimitsReachedError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceNoEditorSeatError,
|
||||
WorkspaceNotEnoughPermissionsError,
|
||||
WorkspaceProjectMoveInvalidError,
|
||||
WorkspaceReadOnlyError,
|
||||
WorkspacesNotEnabledError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
import {
|
||||
MaybeProjectContext,
|
||||
MaybeUserContext,
|
||||
ProjectContext,
|
||||
WorkspaceContext
|
||||
MaybeWorkspaceContext
|
||||
} 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 {
|
||||
ensureWorkspaceProjectCanBeCreatedFragment,
|
||||
ensureWorkspaceRoleAndSessionFragment,
|
||||
ensureWorkspacesEnabledFragment
|
||||
} from '../../fragments/workspaces.js'
|
||||
@@ -39,8 +41,9 @@ type PolicyLoaderKeys =
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspacePlan
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceLimits
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceProjectCount
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceSeat
|
||||
|
||||
type PolicyArgs = MaybeUserContext & ProjectContext & WorkspaceContext
|
||||
type PolicyArgs = MaybeUserContext & MaybeProjectContext & MaybeWorkspaceContext
|
||||
|
||||
type PolicyErrors =
|
||||
| InstanceType<typeof ProjectNotFoundError>
|
||||
@@ -53,6 +56,8 @@ type PolicyErrors =
|
||||
| InstanceType<typeof WorkspaceProjectMoveInvalidError>
|
||||
| InstanceType<typeof ServerNoSessionError>
|
||||
| InstanceType<typeof ServerNoAccessError>
|
||||
| InstanceType<typeof WorkspaceNoEditorSeatError>
|
||||
| InstanceType<typeof WorkspaceNotEnoughPermissionsError>
|
||||
|
||||
export const canMoveToWorkspacePolicy: AuthPolicy<
|
||||
PolicyLoaderKeys,
|
||||
@@ -64,48 +69,47 @@ export const canMoveToWorkspacePolicy: AuthPolicy<
|
||||
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)
|
||||
if (projectId) {
|
||||
// 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 ensuredWorkspaceAccess = await ensureWorkspaceRoleAndSessionFragment(loaders)(
|
||||
{
|
||||
const ensuredProjectRole = await ensureMinimumProjectRoleFragment(loaders)({
|
||||
userId: userId!,
|
||||
projectId,
|
||||
role: Roles.Stream.Owner
|
||||
})
|
||||
if (ensuredProjectRole.isErr) return err(ensuredProjectRole.error)
|
||||
}
|
||||
|
||||
if (workspaceId) {
|
||||
const ensuredWorkspaceAccess = await ensureWorkspaceRoleAndSessionFragment(
|
||||
loaders
|
||||
)({
|
||||
userId: userId!,
|
||||
workspaceId,
|
||||
role: Roles.Workspace.Admin
|
||||
})
|
||||
if (ensuredWorkspaceAccess.isErr) return err(ensuredWorkspaceAccess.error)
|
||||
|
||||
// Ensure workspace accepts new projects
|
||||
const ensuredProjectsAccepted = await ensureWorkspaceProjectCanBeCreatedFragment(
|
||||
loaders
|
||||
)({
|
||||
workspaceId,
|
||||
userId
|
||||
})
|
||||
if (ensuredProjectsAccepted.isErr) {
|
||||
return err(ensuredProjectsAccepted.error)
|
||||
}
|
||||
)
|
||||
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' } }))
|
||||
return ok()
|
||||
}
|
||||
|
||||
@@ -10,12 +10,13 @@ import {
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { getProjectFake } from '../../../tests/fakes.js'
|
||||
|
||||
describe('canReadProjectSettingsPolicy', () => {
|
||||
const buildSUT = (overrides?: OverridesOf<typeof canReadProjectSettingsPolicy>) =>
|
||||
canReadProjectSettingsPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -35,7 +36,7 @@ describe('canReadProjectSettingsPolicy', () => {
|
||||
overrides?: OverridesOf<typeof canReadProjectSettingsPolicy>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
|
||||
@@ -10,12 +10,13 @@ import {
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { canReadProjectWebhooksPolicy } from './canReadWebhooks.js'
|
||||
import { getProjectFake } from '../../../tests/fakes.js'
|
||||
|
||||
describe('canReadProjectWebhooksPolicy', () => {
|
||||
const buildSUT = (overrides?: OverridesOf<typeof canReadProjectWebhooksPolicy>) =>
|
||||
canReadProjectWebhooksPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -35,7 +36,7 @@ describe('canReadProjectWebhooksPolicy', () => {
|
||||
overrides?: OverridesOf<typeof canReadProjectWebhooksPolicy>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
|
||||
@@ -9,12 +9,13 @@ import {
|
||||
ServerNoSessionError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { getProjectFake } from '../../../tests/fakes.js'
|
||||
|
||||
// Default deps allow test to succeed, this makes it so that we need to override less of them
|
||||
const buildSUT = (overrides?: Partial<Parameters<typeof canUpdateProjectPolicy>[0]>) =>
|
||||
canUpdateProjectPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -33,7 +34,7 @@ const buildWorkspaceSUT = (
|
||||
overrides?: Partial<Parameters<typeof canUpdateProjectPolicy>[0]>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
@@ -47,11 +48,15 @@ const buildWorkspaceSUT = (
|
||||
getWorkspaceSsoProvider: async () => ({
|
||||
providerId: 'provider-id'
|
||||
}),
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date()
|
||||
}),
|
||||
getWorkspaceSsoSession: async () => {
|
||||
const validUntil = new Date()
|
||||
validUntil.setDate(validUntil.getDate() + 7)
|
||||
return {
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil
|
||||
}
|
||||
},
|
||||
...overrides
|
||||
})
|
||||
|
||||
|
||||
@@ -2,11 +2,7 @@ import { err, ok } from 'true-myth/result'
|
||||
import { MaybeUserContext, ProjectContext } from '../../domain/context.js'
|
||||
import { AuthPolicy } from '../../domain/policies.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import { ensureMinimumServerRoleFragment } from '../../fragments/server.js'
|
||||
import {
|
||||
ensureMinimumProjectRoleFragment,
|
||||
ensureProjectWorkspaceAccessFragment
|
||||
} from '../../fragments/projects.js'
|
||||
import { ensureImplicitProjectMemberWithWriteAccessFragment } from '../../fragments/projects.js'
|
||||
import { Loaders } from '../../domain/loaders.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
@@ -38,29 +34,16 @@ export const canUpdateProjectPolicy: AuthPolicy<
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId }) => {
|
||||
const ensuredServerRole = await ensureMinimumServerRoleFragment(loaders)({
|
||||
// Ensure proper project owner level write access
|
||||
const ensuredWriteAccess = await ensureImplicitProjectMemberWithWriteAccessFragment(
|
||||
loaders
|
||||
)({
|
||||
userId,
|
||||
role: Roles.Server.User
|
||||
})
|
||||
if (ensuredServerRole.isErr) {
|
||||
return err(ensuredServerRole.error)
|
||||
}
|
||||
|
||||
const ensuredWorkspaceAccess = await ensureProjectWorkspaceAccessFragment(loaders)({
|
||||
userId: userId!,
|
||||
projectId
|
||||
})
|
||||
if (ensuredWorkspaceAccess.isErr) {
|
||||
return err(ensuredWorkspaceAccess.error)
|
||||
}
|
||||
|
||||
const ensuredProjectRole = await ensureMinimumProjectRoleFragment(loaders)({
|
||||
userId: userId!,
|
||||
projectId,
|
||||
role: Roles.Stream.Owner
|
||||
})
|
||||
if (ensuredProjectRole.isErr) {
|
||||
return err(ensuredProjectRole.error)
|
||||
if (ensuredWriteAccess.isErr) {
|
||||
return err(ensuredWriteAccess.error)
|
||||
}
|
||||
|
||||
return ok()
|
||||
|
||||
@@ -4,6 +4,7 @@ import { canUpdateProjectAllowPublicCommentsPolicy } from './canUpdateAllowPubli
|
||||
import { parseFeatureFlags } from '../../../environment/index.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import { ProjectNoAccessError } from '../../domain/authErrors.js'
|
||||
import { getProjectFake } from '../../../tests/fakes.js'
|
||||
|
||||
describe('canUpdateProjectAllowPublicCommentsPolicy', () => {
|
||||
const buildSUT = (
|
||||
@@ -11,7 +12,7 @@ describe('canUpdateProjectAllowPublicCommentsPolicy', () => {
|
||||
) =>
|
||||
canUpdateProjectAllowPublicCommentsPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -39,7 +40,7 @@ describe('canUpdateProjectAllowPublicCommentsPolicy', () => {
|
||||
|
||||
it('succeeds if discoverable project', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: true,
|
||||
@@ -72,7 +73,7 @@ describe('canUpdateProjectAllowPublicCommentsPolicy', () => {
|
||||
|
||||
it('fails if project is neither public nor discoverable', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { canArchiveProjectCommentPolicy } from './canArchive.js'
|
||||
import { OverridesOf } from '../../../../tests/helpers/types.js'
|
||||
import { parseFeatureFlags } from '../../../../environment/index.js'
|
||||
import { getCommentFake, getProjectFake } from '../../../../tests/fakes.js'
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
import {
|
||||
CommentNotFoundError,
|
||||
ProjectNoAccessError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
|
||||
describe('canArchiveProjectCommentPolicy', () => {
|
||||
const buildSUT = (overrides?: OverridesOf<typeof canArchiveProjectCommentPolicy>) =>
|
||||
canArchiveProjectCommentPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Reviewer,
|
||||
getServerRole: async () => Roles.Server.User,
|
||||
getWorkspace: async () => null,
|
||||
getWorkspaceRole: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
authorId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
}),
|
||||
...overrides
|
||||
})
|
||||
|
||||
const buildWorkspaceSUT = (
|
||||
overrides?: OverridesOf<typeof canArchiveProjectCommentPolicy>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
allowPublicComments: false
|
||||
}),
|
||||
getProjectRole: async () => null,
|
||||
getWorkspace: async () => ({
|
||||
id: 'workspace-id',
|
||||
slug: 'workspace-slug'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getWorkspaceSsoProvider: async () => ({
|
||||
providerId: 'provider-id'
|
||||
}),
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date()
|
||||
}),
|
||||
...overrides
|
||||
})
|
||||
|
||||
it('can archive own comment', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it("can't archive own comment w/o project roles", async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it("can archive others' comments if owner", async () => {
|
||||
const sut = buildSUT({
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
authorId: 'other-user-id',
|
||||
projectId: 'project-id'
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Owner
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it("can't archive others' comments if not owner", async () => {
|
||||
const sut = buildSUT({
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
authorId: 'other-user-id',
|
||||
projectId: 'project-id'
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Contributor
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user not defined', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoSessionError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getServerRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if comment not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getComment: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'aaaaaaaaaaaa',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: CommentNotFoundError.code
|
||||
})
|
||||
})
|
||||
|
||||
describe('with workspace project', () => {
|
||||
it('can archive own comment', async () => {
|
||||
const sut = buildWorkspaceSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it("can archive others' comments if workspace admin", async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
authorId: 'other-user-id',
|
||||
projectId: 'project-id'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Admin
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it("can archive others' comments as admin w/o sso, if not needed", async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
authorId: 'other-user-id',
|
||||
projectId: 'project-id'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Admin,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it("can arhive others' comments if explicit project owner", async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
authorId: 'other-user-id',
|
||||
projectId: 'project-id'
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Owner
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it("can't archive others' comments if not owner", async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
authorId: 'other-user-id',
|
||||
projectId: 'project-id'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it("can't archive others' comments as owner, if no sso session", async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
authorId: 'other-user-id',
|
||||
projectId: 'project-id'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Admin,
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it("can't archive others' comments as owner, if sso session expired", async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
authorId: 'other-user-id',
|
||||
projectId: 'project-id'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Admin,
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date(0)
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { AuthPolicy } from '../../../domain/policies.js'
|
||||
import {
|
||||
CommentContext,
|
||||
MaybeUserContext,
|
||||
ProjectContext
|
||||
} from '../../../domain/context.js'
|
||||
import { Loaders } from '../../../domain/loaders.js'
|
||||
import {
|
||||
CommentNotFoundError,
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
import { ensureImplicitProjectMemberWithWriteAccessFragment } from '../../../fragments/projects.js'
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
import { canCreateProjectCommentPolicy } from './canCreate.js'
|
||||
|
||||
export const canArchiveProjectCommentPolicy: AuthPolicy<
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getComment
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole,
|
||||
MaybeUserContext & CommentContext & ProjectContext,
|
||||
InstanceType<
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof WorkspaceSsoSessionNoAccessError
|
||||
| typeof CommentNotFoundError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, commentId, projectId }) => {
|
||||
// Includes canCreate check (checks general comment write access,
|
||||
// cause just owning a comment is not enough, if you've been banned from it)
|
||||
const canCreate = await canCreateProjectCommentPolicy(loaders)({
|
||||
userId,
|
||||
projectId
|
||||
})
|
||||
if (canCreate.isErr) {
|
||||
return err(canCreate.error)
|
||||
}
|
||||
|
||||
// Check that comment exists
|
||||
const comment = await loaders.getComment({ commentId, projectId })
|
||||
if (!comment) return err(new CommentNotFoundError())
|
||||
|
||||
// If user is owner, no extra checks necessary
|
||||
if (comment.authorId === userId) return ok()
|
||||
|
||||
// Otherwise Ensure proper project owner level write access
|
||||
const ensuredWriteAccess = await ensureImplicitProjectMemberWithWriteAccessFragment(
|
||||
loaders
|
||||
)({
|
||||
userId,
|
||||
projectId,
|
||||
role: Roles.Stream.Owner
|
||||
})
|
||||
if (ensuredWriteAccess.isErr) {
|
||||
return err(ensuredWriteAccess.error)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { OverridesOf } from '../../../../tests/helpers/types.js'
|
||||
import { canCreateProjectCommentPolicy } from './canCreate.js'
|
||||
import { parseFeatureFlags } from '../../../../environment/index.js'
|
||||
import { getProjectFake } from '../../../../tests/fakes.js'
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
|
||||
describe('canCreateProjectCommentPolicy', () => {
|
||||
const buildSUT = (overrides?: OverridesOf<typeof canCreateProjectCommentPolicy>) =>
|
||||
canCreateProjectCommentPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Reviewer,
|
||||
getServerRole: async () => Roles.Server.User,
|
||||
getWorkspace: async () => null,
|
||||
getWorkspaceRole: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
...overrides
|
||||
})
|
||||
|
||||
const buildWorkspaceSUT = (
|
||||
overrides?: OverridesOf<typeof canCreateProjectCommentPolicy>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
allowPublicComments: false
|
||||
}),
|
||||
getProjectRole: async () => null,
|
||||
getWorkspace: async () => ({
|
||||
id: 'workspace-id',
|
||||
slug: 'workspace-slug'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getWorkspaceSsoProvider: async () => ({
|
||||
providerId: 'provider-id'
|
||||
}),
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date()
|
||||
}),
|
||||
...overrides
|
||||
})
|
||||
|
||||
it('succeeds w/ explicit project role', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('succeeds w/o project role if public and public comments allowed', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: true,
|
||||
allowPublicComments: true
|
||||
}),
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails w/o project role', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails w/o project role if public, but no public comments allowed', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => null,
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: true,
|
||||
allowPublicComments: false
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user undefined', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: undefined,
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoSessionError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getServerRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if project not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNotFoundError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails w/o project role, even if admin', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => null,
|
||||
getServerRole: async () => Roles.Server.Admin
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
describe('with workspace project', () => {
|
||||
it('succeeds w/ implicit project role', async () => {
|
||||
const sut = buildWorkspaceSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('succeeds w/ explicit project role, if guest', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getProjectRole: async () => Roles.Stream.Reviewer,
|
||||
getWorkspaceRole: async () => Roles.Workspace.Guest
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails w/o project role, if only guest', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getProjectRole: async () => null,
|
||||
getWorkspaceRole: async () => Roles.Workspace.Guest
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('succeeds w/o session, if guest w/ explicit role', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getProjectRole: async () => Roles.Stream.Reviewer,
|
||||
getWorkspaceRole: async () => Roles.Workspace.Guest,
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('succeeds w/o session, if not needed', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails w/o session, if needed', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails w/ expired session', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date(Date.now() - 1000)
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { MaybeUserContext, ProjectContext } from '../../../domain/context.js'
|
||||
import { AuthPolicy } from '../../../domain/policies.js'
|
||||
import { ensureMinimumServerRoleFragment } from '../../../fragments/server.js'
|
||||
import { Loaders } from '../../../domain/loaders.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
import { ensureImplicitProjectMemberWithWriteAccessFragment } from '../../../fragments/projects.js'
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
|
||||
export const canCreateProjectCommentPolicy: AuthPolicy<
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole,
|
||||
MaybeUserContext & ProjectContext,
|
||||
InstanceType<
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof WorkspaceSsoSessionNoAccessError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId }) => {
|
||||
// Ensure server access
|
||||
const ensuredServerRole = await ensureMinimumServerRoleFragment(loaders)({
|
||||
userId
|
||||
})
|
||||
if (ensuredServerRole.isErr) {
|
||||
return err(ensuredServerRole.error)
|
||||
}
|
||||
|
||||
// Check if public commenting enabled
|
||||
const project = await loaders.getProject({ projectId })
|
||||
if (!project) return err(new ProjectNotFoundError())
|
||||
const allowPublicCommenting =
|
||||
(project.isPublic || project.isDiscoverable) && project.allowPublicComments
|
||||
if (allowPublicCommenting) return ok()
|
||||
|
||||
// Not public, ensure proper project write access
|
||||
const ensuredWriteAccess = await ensureImplicitProjectMemberWithWriteAccessFragment(
|
||||
loaders
|
||||
)({
|
||||
userId,
|
||||
projectId,
|
||||
role: Roles.Stream.Reviewer
|
||||
})
|
||||
if (ensuredWriteAccess.isErr) {
|
||||
return err(ensuredWriteAccess.error)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { OverridesOf } from '../../../../tests/helpers/types.js'
|
||||
import { parseFeatureFlags } from '../../../../environment/index.js'
|
||||
import { getCommentFake, getProjectFake } from '../../../../tests/fakes.js'
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
import {
|
||||
CommentNoAccessError,
|
||||
CommentNotFoundError,
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
import { canEditProjectCommentPolicy } from './canEdit.js'
|
||||
|
||||
describe('canEditProjectCommentPolicy', () => {
|
||||
const buildSUT = (overrides?: OverridesOf<typeof canEditProjectCommentPolicy>) =>
|
||||
canEditProjectCommentPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Reviewer,
|
||||
getServerRole: async () => Roles.Server.User,
|
||||
getWorkspace: async () => null,
|
||||
getWorkspaceRole: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
projectId: 'project-id',
|
||||
authorId: 'user-id'
|
||||
}),
|
||||
...overrides
|
||||
})
|
||||
|
||||
const buildWorkspaceSUT = (
|
||||
overrides?: OverridesOf<typeof canEditProjectCommentPolicy>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
allowPublicComments: false
|
||||
}),
|
||||
getProjectRole: async () => null,
|
||||
getWorkspace: async () => ({
|
||||
id: 'workspace-id',
|
||||
slug: 'workspace-slug'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getWorkspaceSsoProvider: async () => ({
|
||||
providerId: 'provider-id'
|
||||
}),
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date()
|
||||
}),
|
||||
...overrides
|
||||
})
|
||||
|
||||
it('succeeds w/ explicit project role', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('succeeds w/o project role if public and public comments allowed', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: true,
|
||||
allowPublicComments: true
|
||||
}),
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails if not the author, even if admin', async () => {
|
||||
const sut = buildSUT({
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
projectId: 'project-id',
|
||||
authorId: 'other-user-id'
|
||||
}),
|
||||
getServerRole: async () => Roles.Server.Admin,
|
||||
getProjectRole: async () => Roles.Stream.Owner
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: CommentNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if comment not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getComment: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: CommentNotFoundError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails w/o project role', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails w/o project role if public, but no public comments allowed', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => null,
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: true,
|
||||
allowPublicComments: false
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user undefined', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: undefined,
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoSessionError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getServerRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if project not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNotFoundError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails w/o project role, even if admin', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => null,
|
||||
getServerRole: async () => Roles.Server.Admin
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
describe('with workspace project', () => {
|
||||
it('succeeds w/ implicit project role', async () => {
|
||||
const sut = buildWorkspaceSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('succeeds w/ explicit project role, if guest', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getProjectRole: async () => Roles.Stream.Reviewer,
|
||||
getWorkspaceRole: async () => Roles.Workspace.Guest
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails if not the author, even if admin', async () => {
|
||||
const sut = buildSUT({
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
projectId: 'project-id',
|
||||
authorId: 'other-user-id'
|
||||
}),
|
||||
getServerRole: async () => Roles.Server.Admin,
|
||||
getProjectRole: async () => Roles.Stream.Owner,
|
||||
getWorkspaceRole: async () => Roles.Workspace.Admin
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: CommentNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails w/o project role, if only guest', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getProjectRole: async () => null,
|
||||
getWorkspaceRole: async () => Roles.Workspace.Guest
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('succeeds w/o session, if guest w/ explicit role', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getProjectRole: async () => Roles.Stream.Reviewer,
|
||||
getWorkspaceRole: async () => Roles.Workspace.Guest,
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('succeeds w/o session, if not needed', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails w/o session, if needed', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails w/ expired session', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date(Date.now() - 1000)
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { AuthPolicy } from '../../../domain/policies.js'
|
||||
import {
|
||||
CommentContext,
|
||||
MaybeUserContext,
|
||||
ProjectContext
|
||||
} from '../../../domain/context.js'
|
||||
import { Loaders } from '../../../domain/loaders.js'
|
||||
import {
|
||||
CommentNoAccessError,
|
||||
CommentNotFoundError,
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
import { canCreateProjectCommentPolicy } from './canCreate.js'
|
||||
|
||||
export const canEditProjectCommentPolicy: AuthPolicy<
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getComment
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole,
|
||||
MaybeUserContext & CommentContext & ProjectContext,
|
||||
InstanceType<
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof WorkspaceSsoSessionNoAccessError
|
||||
| typeof CommentNotFoundError
|
||||
| typeof CommentNoAccessError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, commentId, projectId }) => {
|
||||
// Includes canCreate check
|
||||
const canCreate = await canCreateProjectCommentPolicy(loaders)({
|
||||
userId,
|
||||
projectId
|
||||
})
|
||||
if (canCreate.isErr) {
|
||||
return err(canCreate.error)
|
||||
}
|
||||
|
||||
// Check that comment exists
|
||||
const comment = await loaders.getComment({ commentId, projectId })
|
||||
if (!comment) return err(new CommentNotFoundError())
|
||||
|
||||
// Disallow if user is not the author
|
||||
if (comment.authorId !== userId) {
|
||||
return err(
|
||||
new CommentNoAccessError('You do not have access to edit this comment')
|
||||
)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
+14
-15
@@ -1,32 +1,31 @@
|
||||
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 { canCreateModelPolicy } from './canCreate.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'
|
||||
} from '../../../domain/authErrors.js'
|
||||
import { getProjectFake } from '../../../../tests/fakes.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 })
|
||||
}
|
||||
},
|
||||
getProject: getProjectFake({
|
||||
id: cryptoRandomString({ length: 9 }),
|
||||
isPublic: false,
|
||||
isDiscoverable: false,
|
||||
workspaceId: cryptoRandomString({ length: 9 })
|
||||
}),
|
||||
getProjectRole: async () => {
|
||||
return Roles.Stream.Contributor
|
||||
},
|
||||
@@ -0,0 +1,71 @@
|
||||
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 { ensureImplicitProjectMemberWithWriteAccessFragment } from '../../../fragments/projects.js'
|
||||
import { ensureModelCanBeCreatedFragment } from '../../../fragments/workspaces.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 }) => {
|
||||
// Ensure general write access
|
||||
const ensureWriteAccess = await ensureImplicitProjectMemberWithWriteAccessFragment(
|
||||
loaders
|
||||
)({
|
||||
userId,
|
||||
projectId
|
||||
})
|
||||
if (ensureWriteAccess.isErr) {
|
||||
return err(ensureWriteAccess.error)
|
||||
}
|
||||
|
||||
// Ensure (workspace?) accepts models
|
||||
const ensuredModelsAccepted = await ensureModelCanBeCreatedFragment(loaders)({
|
||||
projectId,
|
||||
userId
|
||||
})
|
||||
if (ensuredModelsAccepted.isErr) {
|
||||
return err(ensuredModelsAccepted.error)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
import { parseFeatureFlags } from '../../../../environment/index.js'
|
||||
import { getModelFake, getProjectFake } from '../../../../tests/fakes.js'
|
||||
import {
|
||||
ModelNotFoundError,
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ReservedModelNotDeletableError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
import { canDeleteModelPolicy } from './canDelete.js'
|
||||
|
||||
const buildSUT = (overrides?: Partial<Parameters<typeof canDeleteModelPolicy>[0]>) =>
|
||||
canDeleteModelPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
}),
|
||||
getModel: getModelFake({
|
||||
id: 'model-id',
|
||||
projectId: 'project-id',
|
||||
authorId: 'user-id',
|
||||
name: 'model-name'
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Contributor,
|
||||
getServerRole: async () => Roles.Server.User,
|
||||
getWorkspace: async () => null,
|
||||
getWorkspaceRole: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
...overrides
|
||||
})
|
||||
|
||||
const buildWorkspaceSUT = (
|
||||
overrides?: Partial<Parameters<typeof canDeleteModelPolicy>[0]>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
}),
|
||||
getWorkspace: async () => ({
|
||||
id: 'workspace-id',
|
||||
slug: 'workspace-slug'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getWorkspaceSsoProvider: async () => ({
|
||||
providerId: 'provider-id'
|
||||
}),
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date()
|
||||
}),
|
||||
...overrides
|
||||
})
|
||||
|
||||
describe('canDeleteModelPolicy', () => {
|
||||
it('returns error if user is not logged in', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: undefined,
|
||||
projectId: 'project-id',
|
||||
modelId: 'model-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoSessionError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if user not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getServerRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
modelId: 'model-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if project not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
modelId: 'model-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNotFoundError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if model not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getModel: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
modelId: 'model-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ModelNotFoundError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if model is reserved', async () => {
|
||||
const sut = buildSUT({
|
||||
getModel: getModelFake({
|
||||
id: 'model-id',
|
||||
projectId: 'project-id',
|
||||
name: 'main',
|
||||
authorId: 'user-id'
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
modelId: 'main'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ReservedModelNotDeletableError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if user is not author and not project owner', async () => {
|
||||
const sut = buildSUT({
|
||||
getModel: getModelFake({
|
||||
id: 'model-id',
|
||||
projectId: 'project-id',
|
||||
authorId: 'other-user-id'
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Contributor
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
modelId: 'model-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if not at least contributor', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => Roles.Stream.Reviewer
|
||||
})
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
modelId: 'model-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns ok if permissible', async () => {
|
||||
const sut = buildSUT()
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
modelId: 'model-id'
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('returns ok if not author, but project owner', async () => {
|
||||
const sut = buildSUT({
|
||||
getModel: getModelFake({
|
||||
id: 'model-id',
|
||||
projectId: 'project-id',
|
||||
authorId: 'other-user-id'
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Owner
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
modelId: 'model-id'
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('returns ok if no author, but project owner', async () => {
|
||||
const sut = buildSUT({
|
||||
getModel: getModelFake({
|
||||
id: 'model-id',
|
||||
projectId: 'project-id',
|
||||
authorId: null
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Owner
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
modelId: 'model-id'
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
describe('with workspace project', () => {
|
||||
it('returns ok if permissible', async () => {
|
||||
const sut = buildWorkspaceSUT()
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
modelId: 'model-id'
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('returns ok with implicit owner role', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceRole: async () => Roles.Workspace.Admin,
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
modelId: 'model-id'
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('returns error if invalid workspace and project role', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getProjectRole: async () => Roles.Stream.Reviewer
|
||||
})
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
modelId: 'model-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns ok if no sso configured', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
modelId: 'model-id'
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('returns error if no sso session', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
modelId: 'model-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if sso expired', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date(new Date().getTime() - 1000)
|
||||
})
|
||||
})
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
modelId: 'model-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,93 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import {
|
||||
MaybeUserContext,
|
||||
ModelContext,
|
||||
ProjectContext
|
||||
} from '../../../domain/context.js'
|
||||
import { AuthPolicy } from '../../../domain/policies.js'
|
||||
import {
|
||||
ensureImplicitProjectMemberWithWriteAccessFragment,
|
||||
ensureMinimumProjectRoleFragment
|
||||
} from '../../../fragments/projects.js'
|
||||
import { Loaders } from '../../../domain/loaders.js'
|
||||
import {
|
||||
ReservedModelNotDeletableError,
|
||||
ModelNotFoundError,
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
|
||||
export const canDeleteModelPolicy: AuthPolicy<
|
||||
| typeof Loaders.getModel
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole,
|
||||
ProjectContext & MaybeUserContext & ModelContext,
|
||||
InstanceType<
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof WorkspaceSsoSessionNoAccessError
|
||||
| typeof ModelNotFoundError
|
||||
| typeof ReservedModelNotDeletableError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId, modelId }) => {
|
||||
// Ensure general project write access
|
||||
const ensureWriteAccess = await ensureImplicitProjectMemberWithWriteAccessFragment(
|
||||
loaders
|
||||
)({
|
||||
userId,
|
||||
projectId
|
||||
})
|
||||
if (ensureWriteAccess.isErr) {
|
||||
return err(ensureWriteAccess.error)
|
||||
}
|
||||
|
||||
// Ensure 'main'/'globals' doesn't get deleted
|
||||
const model = await loaders.getModel({
|
||||
projectId,
|
||||
modelId
|
||||
})
|
||||
if (!model) {
|
||||
return err(new ModelNotFoundError())
|
||||
}
|
||||
|
||||
// Model must be owned by author OR user must be project owner
|
||||
if (!model.authorId || model.authorId !== userId) {
|
||||
const ensureProjectOwner = await ensureMinimumProjectRoleFragment(loaders)({
|
||||
userId: userId!,
|
||||
projectId,
|
||||
role: Roles.Stream.Owner
|
||||
})
|
||||
if (ensureProjectOwner.isErr) {
|
||||
return err(ensureProjectOwner.error)
|
||||
}
|
||||
}
|
||||
|
||||
if (model.name === 'main') {
|
||||
return err(
|
||||
new ReservedModelNotDeletableError("The 'main' model cannot be deleted")
|
||||
)
|
||||
}
|
||||
if (model.name === 'globals') {
|
||||
return err(
|
||||
new ReservedModelNotDeletableError("The 'globals' model cannot be deleted")
|
||||
)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
import { parseFeatureFlags } from '../../../../environment/index.js'
|
||||
import { getProjectFake } from '../../../../tests/fakes.js'
|
||||
import { canUpdateModelPolicy } from './canUpdate.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
|
||||
const buildSUT = (overrides?: Partial<Parameters<typeof canUpdateModelPolicy>[0]>) =>
|
||||
canUpdateModelPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Contributor,
|
||||
getServerRole: async () => Roles.Server.User,
|
||||
getWorkspace: async () => null,
|
||||
getWorkspaceRole: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
...overrides
|
||||
})
|
||||
|
||||
const buildWorkspaceSUT = (
|
||||
overrides?: Partial<Parameters<typeof canUpdateModelPolicy>[0]>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
}),
|
||||
getWorkspace: async () => ({
|
||||
id: 'workspace-id',
|
||||
slug: 'workspace-slug'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getWorkspaceSsoProvider: async () => ({
|
||||
providerId: 'provider-id'
|
||||
}),
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date()
|
||||
}),
|
||||
...overrides
|
||||
})
|
||||
|
||||
describe('canUpdateProject', () => {
|
||||
it('returns error if user is not logged in', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: undefined,
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoSessionError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if user not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getServerRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if project not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNotFoundError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if not at least contributor', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => Roles.Stream.Reviewer
|
||||
})
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns ok if permissible', async () => {
|
||||
const sut = buildSUT()
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
describe('with workspace project', () => {
|
||||
it('returns ok if permissible', async () => {
|
||||
const sut = buildWorkspaceSUT()
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('returns ok with implicit owner role', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceRole: async () => Roles.Workspace.Admin,
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('returns error if invalid workspace and project role', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getProjectRole: async () => Roles.Stream.Reviewer
|
||||
})
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns ok if no sso configured', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('returns error if no sso session', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if sso expired', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date(new Date().getTime() - 1000)
|
||||
})
|
||||
})
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { MaybeUserContext, ProjectContext } from '../../../domain/context.js'
|
||||
import { AuthPolicy } from '../../../domain/policies.js'
|
||||
import { ensureImplicitProjectMemberWithWriteAccessFragment } from '../../../fragments/projects.js'
|
||||
import { Loaders } from '../../../domain/loaders.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
|
||||
export const canUpdateModelPolicy: AuthPolicy<
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole,
|
||||
ProjectContext & MaybeUserContext,
|
||||
InstanceType<
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof WorkspaceSsoSessionNoAccessError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId }) => {
|
||||
// Ensure general project write access
|
||||
const ensureWriteAccess = await ensureImplicitProjectMemberWithWriteAccessFragment(
|
||||
loaders
|
||||
)({
|
||||
userId,
|
||||
projectId
|
||||
})
|
||||
if (ensureWriteAccess.isErr) {
|
||||
return err(ensureWriteAccess.error)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
@@ -13,17 +13,12 @@ import {
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import {
|
||||
ensureWorkspaceProjectCanBeCreatedFragment,
|
||||
ensureWorkspaceRoleAndSessionFragment,
|
||||
ensureWorkspacesEnabledFragment
|
||||
} from '../../fragments/workspaces.js'
|
||||
import { hasEditorSeat } from '../../checks/workspaceSeat.js'
|
||||
import { MaybeUserContext, WorkspaceContext } from '../../domain/context.js'
|
||||
import {
|
||||
isNewWorkspacePlan,
|
||||
isWorkspacePlanStatusReadOnly
|
||||
} from '../../../workspaces/index.js'
|
||||
import { ensureMinimumServerRoleFragment } from '../../fragments/server.js'
|
||||
import { hasMinimumWorkspaceRole } from '../../checks/workspaceRole.js'
|
||||
|
||||
export const canCreateWorkspaceProjectPolicy: AuthPolicy<
|
||||
| 'getEnv'
|
||||
@@ -68,47 +63,16 @@ export const canCreateWorkspaceProjectPolicy: AuthPolicy<
|
||||
return err(ensuredWorkspaceAccess.error)
|
||||
}
|
||||
|
||||
// guests cannot create projects in the workspace
|
||||
const isNotGuest = await hasMinimumWorkspaceRole(loaders)({
|
||||
userId: userId!,
|
||||
// Ensure workspace accepts new projects
|
||||
const ensuredProjectsAccepted = await ensureWorkspaceProjectCanBeCreatedFragment(
|
||||
loaders
|
||||
)({
|
||||
workspaceId,
|
||||
role: Roles.Workspace.Member
|
||||
userId
|
||||
})
|
||||
|
||||
if (!isNotGuest)
|
||||
return err(
|
||||
new WorkspaceNotEnoughPermissionsError({
|
||||
message: 'Guests cannot create projects in the workspace'
|
||||
})
|
||||
)
|
||||
|
||||
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: userId!,
|
||||
workspaceId
|
||||
})
|
||||
if (!isEditor) return err(new WorkspaceNoEditorSeatError())
|
||||
if (ensuredProjectsAccepted.isErr) {
|
||||
return err(ensuredProjectsAccepted.error)
|
||||
}
|
||||
|
||||
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' } }))
|
||||
return ok()
|
||||
}
|
||||
|
||||
@@ -1,20 +1,37 @@
|
||||
import { merge } from 'lodash'
|
||||
import { Project } from '../authz/domain/projects/types.js'
|
||||
import { Comment } from '../authz/domain/comments/types.js'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { Model } from '../authz/domain/models/types.js'
|
||||
|
||||
export const fakeGetFactory =
|
||||
<T extends Record<string, unknown>>(defaults: T) =>
|
||||
<T extends Record<string, unknown>>(defaults: () => T) =>
|
||||
(overrides?: Partial<T>) =>
|
||||
(): Promise<T> => {
|
||||
if (overrides) {
|
||||
return Promise.resolve(merge(defaults, overrides))
|
||||
return Promise.resolve(merge({}, defaults(), overrides))
|
||||
}
|
||||
return Promise.resolve(defaults)
|
||||
|
||||
return Promise.resolve(defaults())
|
||||
}
|
||||
|
||||
export const getProjectFake = fakeGetFactory<Project>({
|
||||
export const getProjectFake = fakeGetFactory<Project>(() => ({
|
||||
id: nanoid(10),
|
||||
isPublic: false,
|
||||
isDiscoverable: false,
|
||||
workspaceId: null
|
||||
})
|
||||
workspaceId: null,
|
||||
allowPublicComments: false
|
||||
}))
|
||||
|
||||
export const getCommentFake = fakeGetFactory<Comment>(() => ({
|
||||
id: nanoid(10),
|
||||
authorId: nanoid(10),
|
||||
projectId: nanoid(10)
|
||||
}))
|
||||
|
||||
export const getModelFake = fakeGetFactory<Model>(() => ({
|
||||
id: nanoid(10),
|
||||
projectId: nanoid(10),
|
||||
name: nanoid(20),
|
||||
authorId: nanoid(10)
|
||||
}))
|
||||
|
||||
@@ -146,6 +146,31 @@ export const isSelfServeAvailablePlan = (plan: WorkspacePlans): boolean => {
|
||||
}
|
||||
}
|
||||
|
||||
export const isPaidPlan = (plan: WorkspacePlans): boolean => {
|
||||
switch (plan) {
|
||||
case 'team':
|
||||
case 'teamUnlimited':
|
||||
case 'pro':
|
||||
case 'proUnlimited':
|
||||
return true
|
||||
case 'free':
|
||||
case 'starter':
|
||||
case 'plus':
|
||||
case 'business':
|
||||
case 'starterInvoiced':
|
||||
case 'plusInvoiced':
|
||||
case 'businessInvoiced':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
return false
|
||||
|
||||
default:
|
||||
throwUncoveredError(plan)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BILLING INTERVALS
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user