Skip to content

Commit fd4c580

Browse files
fix(self-host): reconcile storage and allowlists
1 parent 6ca1662 commit fd4c580

14 files changed

Lines changed: 145 additions & 30 deletions

apps/sim/hooks/use-permission-config.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
resolveIntegrationAvailabilityStateForVisibility,
1616
} from '@/lib/integrations/availability'
1717
import { isBlockTypeAccessControlExempt } from '@/lib/permission-groups/block-access'
18+
import { intersectIntegrationAllowlists } from '@/lib/permission-groups/integration-allowlist'
1819
import {
1920
DEFAULT_PERMISSION_GROUP_CONFIG,
2021
type PermissionGroupConfig,
@@ -51,16 +52,6 @@ function useAllowedIntegrationsFromEnv() {
5152
})
5253
}
5354

54-
/**
55-
* Intersects two allowlists. If either is null (unrestricted), returns the other.
56-
* If both are set, returns only items present in both.
57-
*/
58-
function intersectAllowlists(a: string[] | null, b: string[] | null): string[] | null {
59-
if (a === null) return b
60-
if (b === null) return a.map((i) => i.toLowerCase())
61-
return a.map((i) => i.toLowerCase()).filter((i) => b.includes(i))
62-
}
63-
6455
export function usePermissionConfig(): PermissionConfigResult {
6556
const params = useParams()
6657
const workspaceId = typeof params?.workspaceId === 'string' ? params.workspaceId : undefined
@@ -84,7 +75,7 @@ export function usePermissionConfig(): PermissionConfigResult {
8475

8576
const mergedAllowedIntegrations = useMemo(() => {
8677
const envAllowlist = envAllowlistData?.allowedIntegrations ?? null
87-
return intersectAllowlists(config.allowedIntegrations, envAllowlist)
78+
return intersectIntegrationAllowlists(config.allowedIntegrations, envAllowlist)
8879
}, [config.allowedIntegrations, envAllowlistData])
8980

9081
const integrationAvailability = useMemo(() => {

apps/sim/lib/copilot/chat/payload.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* @vitest-environment node
33
*/
4-
import { workflowsUtilsMock } from '@sim/testing'
4+
import { envFlagsMockFns, resetEnvFlagsMock, workflowsUtilsMock } from '@sim/testing'
55
import { beforeEach, describe, expect, it, vi } from 'vitest'
66

77
const {
@@ -146,6 +146,7 @@ import {
146146
describe('buildIntegrationToolSchemas', () => {
147147
beforeEach(() => {
148148
vi.clearAllMocks()
149+
resetEnvFlagsMock()
149150
clearIntegrationToolSchemaCacheForTests()
150151
mockCreateUserToolSchema.mockReturnValue({ type: 'object', properties: {} })
151152
mockIsIntegrationDeploymentAvailable.mockReturnValue(true)
@@ -239,6 +240,24 @@ describe('buildIntegrationToolSchemas', () => {
239240
expect(toolSchemas.some((tool) => tool.name === 'brandfetch_search')).toBe(true)
240241
})
241242

243+
it('intersects workspace and deployment integration allowlists', async () => {
244+
mockGetHighestPrioritySubscription.mockResolvedValue({ plan: 'pro', status: 'active' })
245+
mockGetUserPermissionConfig.mockResolvedValue({
246+
allowedIntegrations: ['gmail', 'brandfetch'],
247+
})
248+
envFlagsMockFns.getAllowedIntegrationsFromEnv.mockReturnValue(['brandfetch'])
249+
250+
const toolSchemas = await buildIntegrationToolSchemas(
251+
'user-intersection',
252+
undefined,
253+
{ schemaSurface: 'copilot' },
254+
'workspace-1'
255+
)
256+
257+
expect(toolSchemas.some((tool) => tool.name === 'gmail_send')).toBe(false)
258+
expect(toolSchemas.some((tool) => tool.name === 'brandfetch_search')).toBe(true)
259+
})
260+
242261
it('keeps a limited integration callable without advertising OAuth', async () => {
243262
mockGetHighestPrioritySubscription.mockResolvedValue({ plan: 'pro', status: 'active' })
244263
mockIsOAuthServiceDeploymentAvailable.mockImplementation(

apps/sim/lib/copilot/chat/payload.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
isIntegrationDeploymentAvailableForVisibility,
2727
isOAuthServiceDeploymentAvailable,
2828
} from '@/lib/integrations/availability.server'
29+
import { intersectIntegrationAllowlists } from '@/lib/permission-groups/integration-allowlist'
2930
import { trackChatUpload } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
3031
import { buildArchiveExtractGuidance, isArchiveFileName } from '@/lib/uploads/utils/file-utils'
3132

@@ -199,7 +200,10 @@ async function buildIntegrationToolSchemasUncached(
199200
if (workspaceId) {
200201
const { getUserPermissionConfig } = await import('@/ee/access-control/utils/permission-check')
201202
const permissionConfig = await getUserPermissionConfig(userId, workspaceId)
202-
allowedIntegrations = permissionConfig?.allowedIntegrations ?? allowedIntegrations
203+
allowedIntegrations = intersectIntegrationAllowlists(
204+
permissionConfig?.allowedIntegrations ?? null,
205+
allowedIntegrations
206+
)
203207
}
204208
const allowedIntegrationTypes = allowedIntegrations
205209
? new Set(allowedIntegrations.map((integration) => integration.toLowerCase()))

apps/sim/lib/copilot/chat/process-contents.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import type { TraceSpan } from '@/lib/logs/types'
3131
import { mcpService } from '@/lib/mcp/service'
3232
import { createMcpToolId } from '@/lib/mcp/utils'
3333
import { isBlockTypeAccessControlExempt } from '@/lib/permission-groups/block-access'
34+
import { intersectIntegrationAllowlists } from '@/lib/permission-groups/integration-allowlist'
3435
import { getColumnId } from '@/lib/table/column-keys'
3536
import { getRowsByIds } from '@/lib/table/rows/service'
3637
import { getTableById } from '@/lib/table/service'
@@ -606,8 +607,10 @@ async function processBlockMetadata(
606607
userId && workspaceId ? getUserPermissionConfig(userId, workspaceId) : null,
607608
userId ? getBlockVisibilityForCopilot(userId, workspaceId) : null,
608609
])
609-
const allowedIntegrations =
610-
permissionConfig?.allowedIntegrations ?? getAllowedIntegrationsFromEnv()
610+
const allowedIntegrations = intersectIntegrationAllowlists(
611+
permissionConfig?.allowedIntegrations ?? null,
612+
getAllowedIntegrationsFromEnv()
613+
)
611614
if (!isIntegrationDeploymentAvailableForVisibility(blockId, visibility)) {
612615
logger.debug('Block unavailable for this deployment', { blockId })
613616
return null

apps/sim/lib/copilot/tools/handlers/integration-tools.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types'
77
import { getAllowedIntegrationsFromEnv } from '@/lib/core/config/env-flags'
88
import { isIntegrationDeploymentAvailableForVisibility } from '@/lib/integrations/availability.server'
9+
import { intersectIntegrationAllowlists } from '@/lib/permission-groups/integration-allowlist'
910
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
1011
import { stripVersionSuffix } from '@/tools/utils'
1112

@@ -24,8 +25,10 @@ export async function executeListIntegrationTools(
2425
const permissionConfig = context.workspaceId
2526
? await getUserPermissionConfig(context.userId, context.workspaceId)
2627
: null
27-
const allowedIntegrations =
28-
permissionConfig?.allowedIntegrations ?? getAllowedIntegrationsFromEnv()
28+
const allowedIntegrations = intersectIntegrationAllowlists(
29+
permissionConfig?.allowedIntegrations ?? null,
30+
getAllowedIntegrationsFromEnv()
31+
)
2932
const all = filterExposedIntegrationTools(
3033
getExposedIntegrationTools(),
3134
vis,

apps/sim/lib/copilot/tools/handlers/oauth.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { isServiceAccountProviderId } from '@/lib/credentials/service-account-pr
88
import { isOAuthServiceAllowedByIntegrationTypes } from '@/lib/integrations/availability'
99
import { isOAuthServiceDeploymentAvailable } from '@/lib/integrations/availability.server'
1010
import { getAllOAuthServices } from '@/lib/oauth/utils'
11+
import { intersectIntegrationAllowlists } from '@/lib/permission-groups/integration-allowlist'
1112
import type { WorkspaceAccess } from '@/lib/workspaces/permissions/utils'
1213
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
1314

@@ -48,8 +49,10 @@ export async function executeOAuthGetAuthLink(
4849
'write'
4950
)
5051
const permissionConfig = await getUserPermissionConfig(context.userId, context.workspaceId)
51-
const configuredAllowedIntegrations =
52-
permissionConfig?.allowedIntegrations ?? getAllowedIntegrationsFromEnv()
52+
const configuredAllowedIntegrations = intersectIntegrationAllowlists(
53+
permissionConfig?.allowedIntegrations ?? null,
54+
getAllowedIntegrationsFromEnv()
55+
)
5356
const allowedIntegrationTypes = configuredAllowedIntegrations
5457
? new Set(configuredAllowedIntegrations.map((type) => type.toLowerCase()))
5558
: null

apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { getAllowedIntegrationsFromEnv, isHosted } from '@/lib/core/config/env-f
99
import { isIntegrationDeploymentAvailableForVisibility } from '@/lib/integrations/availability.server'
1010
import { getServiceAccountProviderForProviderId } from '@/lib/oauth/utils'
1111
import { isBlockTypeAccessControlExempt } from '@/lib/permission-groups/block-access'
12+
import { intersectIntegrationAllowlists } from '@/lib/permission-groups/integration-allowlist'
1213
import { isCustomBlockType } from '@/blocks/custom/build-config'
1314
import { getBlock } from '@/blocks/registry'
1415
import { AuthMode, type BlockConfig, isHiddenFromDisplay } from '@/blocks/types'
@@ -126,8 +127,10 @@ export const getBlocksMetadataServerTool: BaseServerTool<
126127
context?.userId && context?.workspaceId
127128
? await getUserPermissionConfig(context.userId, context.workspaceId)
128129
: null
129-
const allowedIntegrations =
130-
permissionConfig?.allowedIntegrations ?? getAllowedIntegrationsFromEnv()
130+
const allowedIntegrations = intersectIntegrationAllowlists(
131+
permissionConfig?.allowedIntegrations ?? null,
132+
getAllowedIntegrationsFromEnv()
133+
)
131134
const visibility = overlayVisibility()
132135

133136
const result: Record<string, CopilotBlockMetadata> = {}

apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
44
import { getAllowedIntegrationsFromEnv } from '@/lib/core/config/env-flags'
55
import { isIntegrationDeploymentAvailableForVisibility } from '@/lib/integrations/availability.server'
66
import { isBlockTypeAccessControlExempt } from '@/lib/permission-groups/block-access'
7+
import { intersectIntegrationAllowlists } from '@/lib/permission-groups/integration-allowlist'
78
import { getAllBlocks } from '@/blocks/registry'
89
import { overlayVisibility } from '@/blocks/visibility/context'
910
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
@@ -28,8 +29,10 @@ export const getTriggerBlocksServerTool: BaseServerTool<
2829
context?.userId && context?.workspaceId
2930
? await getUserPermissionConfig(context.userId, context.workspaceId)
3031
: null
31-
const allowedIntegrations =
32-
permissionConfig?.allowedIntegrations ?? getAllowedIntegrationsFromEnv()
32+
const allowedIntegrations = intersectIntegrationAllowlists(
33+
permissionConfig?.allowedIntegrations ?? null,
34+
getAllowedIntegrationsFromEnv()
35+
)
3336
const visibility = overlayVisibility()
3437

3538
const triggerBlockIds: string[] = []

apps/sim/lib/copilot/tools/server/user/get-credentials.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { getAccessibleOAuthCredentials } from '@/lib/credentials/environment'
1111
import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils'
1212
import { createIntegrationCredentialVisibility } from '@/lib/integrations/credential-visibility.server'
1313
import { getAllOAuthServices } from '@/lib/oauth'
14+
import { intersectIntegrationAllowlists } from '@/lib/permission-groups/integration-allowlist'
1415
import { checkWorkspaceAccess, type WorkspaceAccess } from '@/lib/workspaces/permissions/utils'
1516
import { overlayVisibility } from '@/blocks/visibility/context'
1617
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
@@ -74,8 +75,10 @@ export const getCredentialsServerTool: BaseServerTool<GetCredentialsParams, any>
7475
const userEmail = userRecord.length > 0 ? userRecord[0]?.email : null
7576

7677
const permissionConfig = workspaceId ? await getUserPermissionConfig(userId, workspaceId) : null
77-
const configuredAllowedIntegrations =
78-
permissionConfig?.allowedIntegrations ?? getAllowedIntegrationsFromEnv()
78+
const configuredAllowedIntegrations = intersectIntegrationAllowlists(
79+
permissionConfig?.allowedIntegrations ?? null,
80+
getAllowedIntegrationsFromEnv()
81+
)
7982
const allowedIntegrationTypes = configuredAllowedIntegrations
8083
? new Set(configuredAllowedIntegrations.map((type) => type.toLowerCase()))
8184
: null

apps/sim/lib/copilot/vfs/workspace-vfs.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ import { createIntegrationCredentialVisibility } from '@/lib/integrations/creden
107107
import { getKnowledgeBases } from '@/lib/knowledge/service'
108108
import { validateMermaidSource } from '@/lib/mermaid/validate'
109109
import { isBlockTypeAccessControlExempt } from '@/lib/permission-groups/block-access'
110+
import { intersectIntegrationAllowlists } from '@/lib/permission-groups/integration-allowlist'
110111
import { getWorkspaceShares } from '@/lib/public-shares/share-manager'
111112
import { listTables } from '@/lib/table/service'
112113
import { listWorkspaceFileFolders } from '@/lib/uploads/contexts/workspace/workspace-file-folder-manager'
@@ -804,8 +805,10 @@ export class WorkspaceVFS {
804805

805806
// Per-viewer gating happens HERE, not in the shared builder: files
806807
// owned by blocks hidden for this viewer are skipped at stamp time.
807-
const configuredAllowedIntegrations =
808-
permissionConfig?.allowedIntegrations ?? getAllowedIntegrationsFromEnv()
808+
const configuredAllowedIntegrations = intersectIntegrationAllowlists(
809+
permissionConfig?.allowedIntegrations ?? null,
810+
getAllowedIntegrationsFromEnv()
811+
)
809812
const allowedIntegrationTypes = configuredAllowedIntegrations
810813
? new Set(configuredAllowedIntegrations.map((type) => type.toLowerCase()))
811814
: null
@@ -2361,8 +2364,10 @@ export class WorkspaceVFS {
23612364
getPersonalAndWorkspaceEnv(userId, workspaceId),
23622365
permissionConfigPromise,
23632366
])
2364-
const configuredAllowedIntegrations =
2365-
permissionConfig?.allowedIntegrations ?? getAllowedIntegrationsFromEnv()
2367+
const configuredAllowedIntegrations = intersectIntegrationAllowlists(
2368+
permissionConfig?.allowedIntegrations ?? null,
2369+
getAllowedIntegrationsFromEnv()
2370+
)
23662371
const credentialVisibility = createIntegrationCredentialVisibility({
23672372
allowedIntegrationTypes: configuredAllowedIntegrations
23682373
? new Set(configuredAllowedIntegrations.map((type) => type.toLowerCase()))

0 commit comments

Comments
 (0)