fix(acc): policy and usage in FE
This commit is contained in:
@@ -104,6 +104,9 @@ graphql(`
|
||||
canReadSettings {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
canReadAccIntegrationSettings {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
canUpdate {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
@@ -179,6 +182,9 @@ const modelCount = computed(() => project.value?.modelCount.totalCount)
|
||||
const commentCount = computed(() => project.value?.commentThreadCount.totalCount)
|
||||
|
||||
const canReadSettings = computed(() => project.value?.permissions.canReadSettings)
|
||||
const canReadAccIntegrationSettings = computed(
|
||||
() => project.value?.permissions.canReadAccIntegrationSettings
|
||||
)
|
||||
const canUpdate = computed(() => project.value?.permissions.canUpdate)
|
||||
const hasRole = computed(() => project.value?.role)
|
||||
const teamUsers = computed(() => project.value?.team.map((t) => t.user) || [])
|
||||
@@ -254,7 +260,7 @@ const pageTabItems = computed((): LayoutPageTabItem[] => {
|
||||
})
|
||||
}
|
||||
|
||||
if (isAccEnabled.value) {
|
||||
if (isAccEnabled.value && canReadAccIntegrationSettings.value?.authorized) {
|
||||
items.push({
|
||||
title: 'ACC',
|
||||
id: 'acc'
|
||||
|
||||
@@ -10,6 +10,7 @@ type ProjectPermissionChecks {
|
||||
canDelete: PermissionCheckResult!
|
||||
canUpdateAllowPublicComments: PermissionCheckResult!
|
||||
canReadSettings: PermissionCheckResult!
|
||||
canReadAccIntegrationSettings: PermissionCheckResult!
|
||||
canReadWebhooks: PermissionCheckResult!
|
||||
canLeave: PermissionCheckResult!
|
||||
canRequestRender: PermissionCheckResult!
|
||||
|
||||
@@ -154,6 +154,7 @@ extend type Project {
|
||||
}
|
||||
|
||||
enum WorkspaceFeatureName {
|
||||
accIntegration
|
||||
domainBasedSecurityPolicies
|
||||
oidcSso
|
||||
hideSpeckleBranding
|
||||
|
||||
@@ -74,6 +74,13 @@ const resolvers: Resolvers = {
|
||||
async create(_parent, args, ctx) {
|
||||
const { input } = args
|
||||
|
||||
const authResult = await ctx.authPolicies.project.canUpdateAccIntegrationSettings(
|
||||
{
|
||||
userId: ctx.userId,
|
||||
projectId: input.projectId
|
||||
}
|
||||
)
|
||||
throwIfAuthNotOk(authResult)
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: input.projectId,
|
||||
resourceAccessRules: ctx.resourceAccessRules,
|
||||
@@ -133,6 +140,13 @@ const resolvers: Resolvers = {
|
||||
async update(_parent, args, ctx) {
|
||||
const { input } = args
|
||||
|
||||
const authResult = await ctx.authPolicies.project.canUpdateAccIntegrationSettings(
|
||||
{
|
||||
userId: ctx.userId,
|
||||
projectId: input.projectId
|
||||
}
|
||||
)
|
||||
throwIfAuthNotOk(authResult)
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: input.projectId,
|
||||
resourceAccessRules: ctx.resourceAccessRules,
|
||||
@@ -150,6 +164,13 @@ const resolvers: Resolvers = {
|
||||
async delete(_parent, args, ctx) {
|
||||
const { input } = args
|
||||
|
||||
const authResult = await ctx.authPolicies.project.canUpdateAccIntegrationSettings(
|
||||
{
|
||||
userId: ctx.userId,
|
||||
projectId: input.projectId
|
||||
}
|
||||
)
|
||||
throwIfAuthNotOk(authResult)
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: input.projectId,
|
||||
resourceAccessRules: ctx.resourceAccessRules,
|
||||
@@ -176,6 +197,11 @@ const resolvers: Resolvers = {
|
||||
async accSyncItems(parent, args, ctx) {
|
||||
const { cursor = null, limit = null } = args
|
||||
|
||||
const authResult = await ctx.authPolicies.project.canReadAccIntegrationSettings({
|
||||
userId: ctx.userId,
|
||||
projectId: parent.id
|
||||
})
|
||||
throwIfAuthNotOk(authResult)
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: parent.id,
|
||||
resourceAccessRules: ctx.resourceAccessRules,
|
||||
@@ -196,6 +222,11 @@ const resolvers: Resolvers = {
|
||||
async accSyncItem(parent, args, ctx) {
|
||||
const { id } = args
|
||||
|
||||
const authResult = await ctx.authPolicies.project.canReadAccIntegrationSettings({
|
||||
userId: ctx.userId,
|
||||
projectId: parent.id
|
||||
})
|
||||
throwIfAuthNotOk(authResult)
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: parent.id,
|
||||
resourceAccessRules: ctx.resourceAccessRules,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Resolvers } from '@/modules/core/graph/generated/graphql'
|
||||
import { Authz } from '@speckle/shared'
|
||||
|
||||
const resolvers: Resolvers = {
|
||||
ProjectPermissionChecks: {
|
||||
async canReadAccIntegrationSettings(parent, _args, ctx) {
|
||||
const authResult = await ctx.authPolicies.project.canReadAccIntegrationSettings({
|
||||
userId: ctx.userId,
|
||||
projectId: parent.projectId
|
||||
})
|
||||
return Authz.toGraphqlResult(authResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default resolvers
|
||||
@@ -2915,6 +2915,7 @@ export type ProjectPermissionChecks = {
|
||||
canMoveToWorkspace: PermissionCheckResult;
|
||||
canPublish: PermissionCheckResult;
|
||||
canRead: PermissionCheckResult;
|
||||
canReadAccIntegrationSettings: PermissionCheckResult;
|
||||
canReadEmbedTokens: PermissionCheckResult;
|
||||
canReadSettings: PermissionCheckResult;
|
||||
canReadWebhooks: PermissionCheckResult;
|
||||
@@ -4992,6 +4993,7 @@ export type WorkspaceEmbedOptions = {
|
||||
};
|
||||
|
||||
export const WorkspaceFeatureName = {
|
||||
AccIntegration: 'accIntegration',
|
||||
DomainBasedSecurityPolicies: 'domainBasedSecurityPolicies',
|
||||
ExclusiveMembership: 'exclusiveMembership',
|
||||
HideSpeckleBranding: 'hideSpeckleBranding',
|
||||
@@ -7341,6 +7343,7 @@ export type ProjectPermissionChecksResolvers<ContextType = GraphQLContext, Paren
|
||||
canMoveToWorkspace?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType, Partial<ProjectPermissionChecksCanMoveToWorkspaceArgs>>;
|
||||
canPublish?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canRead?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canReadAccIntegrationSettings?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canReadEmbedTokens?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canReadSettings?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canReadWebhooks?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AccModuleDisabledError } from '@/modules/acc/errors/acc'
|
||||
import { AutomateModuleDisabledError } from '@/modules/core/errors/automate'
|
||||
import { StreamNotFoundError } from '@/modules/core/errors/stream'
|
||||
import { WorkspacesModuleDisabledError } from '@/modules/core/errors/workspaces'
|
||||
@@ -56,6 +57,8 @@ export const mapAuthToServerError = (e: Authz.AllAuthErrors): BaseError => {
|
||||
return new WorkspacesModuleDisabledError()
|
||||
case Authz.AutomateNotEnabledError.code:
|
||||
return new AutomateModuleDisabledError()
|
||||
case Authz.AccIntegrationNotEnabledError.code:
|
||||
return new AccModuleDisabledError()
|
||||
case Authz.ProjectLastOwnerError.code:
|
||||
case Authz.ReservedModelNotDeletableError.code:
|
||||
return new BadRequestError(e.message)
|
||||
|
||||
@@ -196,6 +196,11 @@ export const AutomateFunctionNotCreatorError = defineAuthError({
|
||||
message: 'You are not the function creator and cannot make changes to it.'
|
||||
})
|
||||
|
||||
export const AccIntegrationNotEnabledError = defineAuthError({
|
||||
code: 'AccIntegrationNotEnabled',
|
||||
message: 'The ACC Integration is not enabled on this server or project'
|
||||
})
|
||||
|
||||
// Resolve all exported error types
|
||||
export type AllAuthErrors = ValueOf<{
|
||||
[key in keyof typeof import('./authErrors.js')]: typeof import('./authErrors.js')[key] extends new (
|
||||
|
||||
@@ -33,6 +33,7 @@ import { canCreateWorkspacePolicy } from './workspace/canCreateWorkspace.js'
|
||||
import { canUseWorkspacePlanFeature } from './workspace/canUseWorkspacePlanFeature.js'
|
||||
import { canEditFunctionPolicy } from './automate/function/canEditFunction.js'
|
||||
import { canUpdateEmbedTokensPolicy } from './project/canUpdateEmbedTokens.js'
|
||||
import { canReadAccIntegrationSettingsPolicy } from './project/canReadAccIntegrationSettings.js'
|
||||
|
||||
export const authPoliciesFactory = (loaders: AllAuthCheckContextLoaders) => ({
|
||||
automate: {
|
||||
@@ -77,7 +78,9 @@ export const authPoliciesFactory = (loaders: AllAuthCheckContextLoaders) => ({
|
||||
canPublish: canPublishPolicy(loaders),
|
||||
canLoad: canLoadPolicy(loaders),
|
||||
canReadEmbedTokens: canUpdateEmbedTokensPolicy(loaders),
|
||||
canUpdateEmbedTokens: canUpdateEmbedTokensPolicy(loaders)
|
||||
canUpdateEmbedTokens: canUpdateEmbedTokensPolicy(loaders),
|
||||
canReadAccIntegrationSettings: canReadAccIntegrationSettingsPolicy(loaders),
|
||||
canUpdateAccIntegrationSettings: canReadAccIntegrationSettingsPolicy(loaders)
|
||||
},
|
||||
workspace: {
|
||||
canCreateProject: canCreateWorkspaceProjectPolicy(loaders),
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import { parseFeatureFlags } from '../../../environment/index.js'
|
||||
import { OverridesOf } from '../../../tests/helpers/types.js'
|
||||
import { canReadAccIntegrationSettingsPolicy } from './canReadAccIntegrationSettings.js'
|
||||
import { getProjectFake, getWorkspaceFake } from '../../../tests/fakes.js'
|
||||
import { assert, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
AccIntegrationNotEnabledError,
|
||||
ProjectNoAccessError,
|
||||
WorkspacePlanNoFeatureAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
|
||||
const buildSUT = (
|
||||
overrides?: OverridesOf<typeof canReadAccIntegrationSettingsPolicy>
|
||||
) => {
|
||||
const workspaceId = cryptoRandomString({ length: 9 })
|
||||
|
||||
return canReadAccIntegrationSettingsPolicy({
|
||||
getEnv: async () => parseFeatureFlags({ FF_ACC_INTEGRATION_ENABLED: 'true' }),
|
||||
getServerRole: async () => {
|
||||
return Roles.Server.User
|
||||
},
|
||||
getAdminOverrideEnabled: async () => {
|
||||
return true
|
||||
},
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId
|
||||
}),
|
||||
getProjectRole: async () => {
|
||||
return Roles.Stream.Contributor
|
||||
},
|
||||
getWorkspace: getWorkspaceFake({
|
||||
id: workspaceId
|
||||
}),
|
||||
getWorkspaceRole: async () => {
|
||||
return Roles.Workspace.Member
|
||||
},
|
||||
getWorkspaceSsoProvider: async () => {
|
||||
return null
|
||||
},
|
||||
getWorkspaceSsoSession: async () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspacePlan: async () => {
|
||||
return {
|
||||
status: 'valid',
|
||||
workspaceId,
|
||||
name: 'enterprise',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
},
|
||||
...overrides
|
||||
})
|
||||
}
|
||||
|
||||
const buildArgs = () => ({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
projectId: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
|
||||
describe('canReadAccIntegrationSettings returns a function, that', () => {
|
||||
it('requires the ACC integration to be enabled', async () => {
|
||||
const result = await buildSUT({
|
||||
getEnv: async () => parseFeatureFlags({ FF_ACC_INTEGRATION_ENABLED: 'false' })
|
||||
})(buildArgs())
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: AccIntegrationNotEnabledError.code
|
||||
})
|
||||
})
|
||||
it('requires the project to belong to a workspace', async () => {
|
||||
const result = await buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id'
|
||||
})
|
||||
})(buildArgs())
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: AccIntegrationNotEnabledError.code
|
||||
})
|
||||
})
|
||||
it('requires the given user to have read access to the project', async () => {
|
||||
const result = await buildSUT({
|
||||
getProjectRole: async () => null
|
||||
})(buildArgs())
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
it('requires the workspace to have an active plan', async () => {
|
||||
const result = await buildSUT({
|
||||
getWorkspacePlan: async () => null
|
||||
})(buildArgs())
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspacePlanNoFeatureAccessError.code
|
||||
})
|
||||
})
|
||||
it('requires the workspace plan to have access to the ACC integration feature', async () => {
|
||||
const result = await buildSUT({
|
||||
getWorkspacePlan: async () => {
|
||||
return {
|
||||
status: 'valid',
|
||||
workspaceId: cryptoRandomString({ length: 9 }),
|
||||
name: 'free',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
}
|
||||
})(buildArgs())
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspacePlanNoFeatureAccessError.code
|
||||
})
|
||||
})
|
||||
it('allows enterprise plans to access the ACC integration feature', async () => {
|
||||
const result = await buildSUT({})(buildArgs())
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,87 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import {
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
ServerNotEnoughPermissionsError,
|
||||
ProjectNotFoundError,
|
||||
ProjectNoAccessError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionNoAccessError,
|
||||
ProjectNotEnoughPermissionsError,
|
||||
WorkspaceNotEnoughPermissionsError,
|
||||
WorkspacePlanNoFeatureAccessError,
|
||||
AccIntegrationNotEnabledError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { MaybeUserContext, ProjectContext } from '../../domain/context.js'
|
||||
import { Loaders } from '../../domain/loaders.js'
|
||||
import { AuthPolicy } from '../../domain/policies.js'
|
||||
import { ensureImplicitProjectMemberWithReadAccessFragment } from '../../fragments/projects.js'
|
||||
import {
|
||||
WorkspacePlanFeatures,
|
||||
workspacePlanHasAccessToFeature
|
||||
} from '../../../workspaces/index.js'
|
||||
|
||||
type PolicyLoaderKeys =
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getAdminOverrideEnabled
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole
|
||||
| typeof Loaders.getWorkspacePlan
|
||||
|
||||
type PolicyArgs = MaybeUserContext & ProjectContext
|
||||
|
||||
type PolicyErrors = InstanceType<
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof ServerNotEnoughPermissionsError
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof WorkspaceSsoSessionNoAccessError
|
||||
| typeof ProjectNotEnoughPermissionsError
|
||||
| typeof WorkspaceNotEnoughPermissionsError
|
||||
| typeof WorkspacePlanNoFeatureAccessError
|
||||
| typeof AccIntegrationNotEnabledError
|
||||
>
|
||||
|
||||
export const canReadAccIntegrationSettingsPolicy: AuthPolicy<
|
||||
PolicyLoaderKeys,
|
||||
PolicyArgs,
|
||||
PolicyErrors
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId }) => {
|
||||
const env = await loaders.getEnv()
|
||||
const project = await loaders.getProject({ projectId })
|
||||
|
||||
if (!env.FF_ACC_INTEGRATION_ENABLED || !project?.workspaceId) {
|
||||
return err(new AccIntegrationNotEnabledError())
|
||||
}
|
||||
|
||||
const ensuredProjectRole = await ensureImplicitProjectMemberWithReadAccessFragment(
|
||||
loaders
|
||||
)({
|
||||
userId,
|
||||
projectId
|
||||
})
|
||||
if (ensuredProjectRole.isErr) {
|
||||
return err(ensuredProjectRole.error)
|
||||
}
|
||||
|
||||
const workspacePlan = await loaders.getWorkspacePlan({
|
||||
workspaceId: project.workspaceId
|
||||
})
|
||||
if (!workspacePlan) return err(new WorkspacePlanNoFeatureAccessError())
|
||||
const canUseFeature = workspacePlanHasAccessToFeature({
|
||||
plan: workspacePlan.name,
|
||||
feature: WorkspacePlanFeatures.AccIntegration
|
||||
})
|
||||
if (!canUseFeature) return err(new WorkspacePlanNoFeatureAccessError())
|
||||
|
||||
return ok()
|
||||
}
|
||||
@@ -22,7 +22,8 @@ export const WorkspacePlanFeatures = <const>{
|
||||
CustomDataRegion: 'workspaceDataRegionSpecificity',
|
||||
HideSpeckleBranding: 'hideSpeckleBranding',
|
||||
ExclusiveMembership: 'exclusiveMembership',
|
||||
EmbedPrivateProjects: 'embedPrivateProjects'
|
||||
EmbedPrivateProjects: 'embedPrivateProjects',
|
||||
AccIntegration: 'accIntegration'
|
||||
}
|
||||
|
||||
export type WorkspacePlanFeatures =
|
||||
@@ -62,6 +63,10 @@ export const WorkspacePlanFeaturesMetadata = (<const>{
|
||||
[WorkspacePlanFeatures.EmbedPrivateProjects]: {
|
||||
displayName: 'Embed private projects',
|
||||
description: 'Embed projects with visibility set to private or workspace'
|
||||
},
|
||||
[WorkspacePlanFeatures.AccIntegration]: {
|
||||
displayName: 'ACC connector',
|
||||
description: 'Configure automatic import of ACC assets into workspace projects'
|
||||
}
|
||||
}) satisfies Record<
|
||||
WorkspacePlanFeatures,
|
||||
@@ -168,7 +173,8 @@ export const WorkspaceUnpaidPlanConfigs: {
|
||||
WorkspacePlanFeatures.SSO,
|
||||
WorkspacePlanFeatures.CustomDataRegion,
|
||||
WorkspacePlanFeatures.HideSpeckleBranding,
|
||||
WorkspacePlanFeatures.ExclusiveMembership
|
||||
WorkspacePlanFeatures.ExclusiveMembership,
|
||||
WorkspacePlanFeatures.AccIntegration
|
||||
],
|
||||
limits: unlimited
|
||||
},
|
||||
@@ -180,7 +186,8 @@ export const WorkspaceUnpaidPlanConfigs: {
|
||||
WorkspacePlanFeatures.SSO,
|
||||
WorkspacePlanFeatures.CustomDataRegion,
|
||||
WorkspacePlanFeatures.HideSpeckleBranding,
|
||||
WorkspacePlanFeatures.ExclusiveMembership
|
||||
WorkspacePlanFeatures.ExclusiveMembership,
|
||||
WorkspacePlanFeatures.AccIntegration
|
||||
],
|
||||
limits: unlimited
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user