mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
feat(access-control): add ALLOWED_INTEGRATIONS env var for self-hosted block restrictions (#3238)
* feat(access-control): add ALLOWED_INTEGRATIONS env var for self-hosted block restrictions * fix(tests): add getAllowedIntegrationsFromEnv mock to agent-handler tests * fix(access-control): add auth to allowlist endpoint, fix loading state race, use accurate error message * fix(access-control): remove auth from allowed-integrations endpoint to match models endpoint pattern * fix(access-control): normalize blockType to lowercase before env allowlist check * fix(access-control): expose merged allowedIntegrations on config to prevent bypass via direct access * consolidate merging of allowed blocks so all callers have it by default * normalize to lower case * added tests * added tests, normalize to lower case * added safety incase userId is missing * fix failing tests
This commit is contained in:
14
apps/sim/app/api/settings/allowed-integrations/route.ts
Normal file
14
apps/sim/app/api/settings/allowed-integrations/route.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { getSession } from '@/lib/auth'
|
||||||
|
import { getAllowedIntegrationsFromEnv } from '@/lib/core/config/feature-flags'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const session = await getSession()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
allowedIntegrations: getAllowedIntegrationsFromEnv(),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -223,13 +223,11 @@ export function Integrations({ onOpenChange, registerCloseHandler }: Integration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group services by provider, filtering by permission config
|
|
||||||
const groupedServices = services.reduce(
|
const groupedServices = services.reduce(
|
||||||
(acc, service) => {
|
(acc, service) => {
|
||||||
// Filter based on allowedIntegrations
|
|
||||||
if (
|
if (
|
||||||
permissionConfig.allowedIntegrations !== null &&
|
permissionConfig.allowedIntegrations !== null &&
|
||||||
!permissionConfig.allowedIntegrations.includes(service.id)
|
!permissionConfig.allowedIntegrations.includes(service.id.replace(/-/g, '_'))
|
||||||
) {
|
) {
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
|
|||||||
265
apps/sim/ee/access-control/utils/permission-check.test.ts
Normal file
265
apps/sim/ee/access-control/utils/permission-check.test.ts
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
/**
|
||||||
|
* @vitest-environment node
|
||||||
|
*/
|
||||||
|
import { databaseMock, drizzleOrmMock, loggerMock } from '@sim/testing'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
const {
|
||||||
|
DEFAULT_PERMISSION_GROUP_CONFIG,
|
||||||
|
mockGetAllowedIntegrationsFromEnv,
|
||||||
|
mockIsOrganizationOnEnterprisePlan,
|
||||||
|
mockGetProviderFromModel,
|
||||||
|
} = vi.hoisted(() => ({
|
||||||
|
DEFAULT_PERMISSION_GROUP_CONFIG: {
|
||||||
|
allowedIntegrations: null,
|
||||||
|
allowedModelProviders: null,
|
||||||
|
hideTraceSpans: false,
|
||||||
|
hideKnowledgeBaseTab: false,
|
||||||
|
hideCopilot: false,
|
||||||
|
hideApiKeysTab: false,
|
||||||
|
hideEnvironmentTab: false,
|
||||||
|
hideFilesTab: false,
|
||||||
|
disableMcpTools: false,
|
||||||
|
disableCustomTools: false,
|
||||||
|
disableSkills: false,
|
||||||
|
hideTemplates: false,
|
||||||
|
disableInvitations: false,
|
||||||
|
hideDeployApi: false,
|
||||||
|
hideDeployMcp: false,
|
||||||
|
hideDeployA2a: false,
|
||||||
|
hideDeployChatbot: false,
|
||||||
|
hideDeployTemplate: false,
|
||||||
|
},
|
||||||
|
mockGetAllowedIntegrationsFromEnv: vi.fn<() => string[] | null>(),
|
||||||
|
mockIsOrganizationOnEnterprisePlan: vi.fn<() => Promise<boolean>>(),
|
||||||
|
mockGetProviderFromModel: vi.fn<(model: string) => string>(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@sim/db', () => databaseMock)
|
||||||
|
vi.mock('@sim/db/schema', () => ({}))
|
||||||
|
vi.mock('@sim/logger', () => loggerMock)
|
||||||
|
vi.mock('drizzle-orm', () => drizzleOrmMock)
|
||||||
|
vi.mock('@/lib/billing', () => ({
|
||||||
|
isOrganizationOnEnterprisePlan: mockIsOrganizationOnEnterprisePlan,
|
||||||
|
}))
|
||||||
|
vi.mock('@/lib/core/config/feature-flags', () => ({
|
||||||
|
getAllowedIntegrationsFromEnv: mockGetAllowedIntegrationsFromEnv,
|
||||||
|
isAccessControlEnabled: false,
|
||||||
|
isHosted: false,
|
||||||
|
}))
|
||||||
|
vi.mock('@/lib/permission-groups/types', () => ({
|
||||||
|
DEFAULT_PERMISSION_GROUP_CONFIG,
|
||||||
|
parsePermissionGroupConfig: (config: unknown) => {
|
||||||
|
if (!config || typeof config !== 'object') return DEFAULT_PERMISSION_GROUP_CONFIG
|
||||||
|
return { ...DEFAULT_PERMISSION_GROUP_CONFIG, ...config }
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
vi.mock('@/providers/utils', () => ({
|
||||||
|
getProviderFromModel: mockGetProviderFromModel,
|
||||||
|
}))
|
||||||
|
|
||||||
|
import {
|
||||||
|
getUserPermissionConfig,
|
||||||
|
IntegrationNotAllowedError,
|
||||||
|
validateBlockType,
|
||||||
|
} from './permission-check'
|
||||||
|
|
||||||
|
describe('IntegrationNotAllowedError', () => {
|
||||||
|
it.concurrent('creates error with correct name and message', () => {
|
||||||
|
const error = new IntegrationNotAllowedError('discord')
|
||||||
|
|
||||||
|
expect(error).toBeInstanceOf(Error)
|
||||||
|
expect(error.name).toBe('IntegrationNotAllowedError')
|
||||||
|
expect(error.message).toContain('discord')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('includes custom reason when provided', () => {
|
||||||
|
const error = new IntegrationNotAllowedError('discord', 'blocked by server policy')
|
||||||
|
|
||||||
|
expect(error.message).toContain('blocked by server policy')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getUserPermissionConfig', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns null when no env allowlist is configured', async () => {
|
||||||
|
mockGetAllowedIntegrationsFromEnv.mockReturnValue(null)
|
||||||
|
|
||||||
|
const config = await getUserPermissionConfig('user-123')
|
||||||
|
|
||||||
|
expect(config).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns config with env allowlist when configured', async () => {
|
||||||
|
mockGetAllowedIntegrationsFromEnv.mockReturnValue(['slack', 'gmail'])
|
||||||
|
|
||||||
|
const config = await getUserPermissionConfig('user-123')
|
||||||
|
|
||||||
|
expect(config).not.toBeNull()
|
||||||
|
expect(config!.allowedIntegrations).toEqual(['slack', 'gmail'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('preserves default values for non-allowlist fields', async () => {
|
||||||
|
mockGetAllowedIntegrationsFromEnv.mockReturnValue(['slack'])
|
||||||
|
|
||||||
|
const config = await getUserPermissionConfig('user-123')
|
||||||
|
|
||||||
|
expect(config!.disableMcpTools).toBe(false)
|
||||||
|
expect(config!.allowedModelProviders).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('env allowlist fallback when userId is absent', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns null allowlist when no userId and no env allowlist', async () => {
|
||||||
|
mockGetAllowedIntegrationsFromEnv.mockReturnValue(null)
|
||||||
|
|
||||||
|
const userId: string | undefined = undefined
|
||||||
|
const permissionConfig = userId ? await getUserPermissionConfig(userId) : null
|
||||||
|
const allowedIntegrations =
|
||||||
|
permissionConfig?.allowedIntegrations ?? mockGetAllowedIntegrationsFromEnv()
|
||||||
|
|
||||||
|
expect(allowedIntegrations).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to env allowlist when no userId is provided', async () => {
|
||||||
|
mockGetAllowedIntegrationsFromEnv.mockReturnValue(['slack', 'gmail'])
|
||||||
|
|
||||||
|
const userId: string | undefined = undefined
|
||||||
|
const permissionConfig = userId ? await getUserPermissionConfig(userId) : null
|
||||||
|
const allowedIntegrations =
|
||||||
|
permissionConfig?.allowedIntegrations ?? mockGetAllowedIntegrationsFromEnv()
|
||||||
|
|
||||||
|
expect(allowedIntegrations).toEqual(['slack', 'gmail'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('env allowlist filters block types when userId is absent', async () => {
|
||||||
|
mockGetAllowedIntegrationsFromEnv.mockReturnValue(['slack', 'gmail'])
|
||||||
|
|
||||||
|
const userId: string | undefined = undefined
|
||||||
|
const permissionConfig = userId ? await getUserPermissionConfig(userId) : null
|
||||||
|
const allowedIntegrations =
|
||||||
|
permissionConfig?.allowedIntegrations ?? mockGetAllowedIntegrationsFromEnv()
|
||||||
|
|
||||||
|
expect(allowedIntegrations).not.toBeNull()
|
||||||
|
expect(allowedIntegrations!.includes('slack')).toBe(true)
|
||||||
|
expect(allowedIntegrations!.includes('discord')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses permission config when userId is present, ignoring env fallback', async () => {
|
||||||
|
mockGetAllowedIntegrationsFromEnv.mockReturnValue(['slack', 'gmail'])
|
||||||
|
|
||||||
|
const config = await getUserPermissionConfig('user-123')
|
||||||
|
|
||||||
|
expect(config).not.toBeNull()
|
||||||
|
expect(config!.allowedIntegrations).toEqual(['slack', 'gmail'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('validateBlockType', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when no env allowlist is configured', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGetAllowedIntegrationsFromEnv.mockReturnValue(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows any block type', async () => {
|
||||||
|
await validateBlockType(undefined, 'google_drive')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows multi-word block types', async () => {
|
||||||
|
await validateBlockType(undefined, 'microsoft_excel')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('always allows start_trigger', async () => {
|
||||||
|
await validateBlockType(undefined, 'start_trigger')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when env allowlist is configured', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGetAllowedIntegrationsFromEnv.mockReturnValue([
|
||||||
|
'slack',
|
||||||
|
'google_drive',
|
||||||
|
'microsoft_excel',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows block types on the allowlist', async () => {
|
||||||
|
await validateBlockType(undefined, 'slack')
|
||||||
|
await validateBlockType(undefined, 'google_drive')
|
||||||
|
await validateBlockType(undefined, 'microsoft_excel')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects block types not on the allowlist', async () => {
|
||||||
|
await expect(validateBlockType(undefined, 'discord')).rejects.toThrow(
|
||||||
|
IntegrationNotAllowedError
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('always allows start_trigger regardless of allowlist', async () => {
|
||||||
|
await validateBlockType(undefined, 'start_trigger')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matches case-insensitively', async () => {
|
||||||
|
await validateBlockType(undefined, 'Slack')
|
||||||
|
await validateBlockType(undefined, 'GOOGLE_DRIVE')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('includes env reason in error when env allowlist is the source', async () => {
|
||||||
|
await expect(validateBlockType(undefined, 'discord')).rejects.toThrow(/ALLOWED_INTEGRATIONS/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('includes env reason even when userId is present if env is the source', async () => {
|
||||||
|
await expect(validateBlockType('user-123', 'discord')).rejects.toThrow(/ALLOWED_INTEGRATIONS/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('service ID to block type normalization', () => {
|
||||||
|
it.concurrent('hyphenated service IDs match underscore block types after normalization', () => {
|
||||||
|
const allowedBlockTypes = [
|
||||||
|
'google_drive',
|
||||||
|
'microsoft_excel',
|
||||||
|
'microsoft_teams',
|
||||||
|
'google_sheets',
|
||||||
|
'google_docs',
|
||||||
|
'google_calendar',
|
||||||
|
'google_forms',
|
||||||
|
'microsoft_planner',
|
||||||
|
]
|
||||||
|
const serviceIds = [
|
||||||
|
'google-drive',
|
||||||
|
'microsoft-excel',
|
||||||
|
'microsoft-teams',
|
||||||
|
'google-sheets',
|
||||||
|
'google-docs',
|
||||||
|
'google-calendar',
|
||||||
|
'google-forms',
|
||||||
|
'microsoft-planner',
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const serviceId of serviceIds) {
|
||||||
|
const normalized = serviceId.replace(/-/g, '_')
|
||||||
|
expect(allowedBlockTypes).toContain(normalized)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('single-word service IDs are unaffected by normalization', () => {
|
||||||
|
const serviceIds = ['slack', 'gmail', 'notion', 'discord', 'jira', 'trello']
|
||||||
|
|
||||||
|
for (const serviceId of serviceIds) {
|
||||||
|
const normalized = serviceId.replace(/-/g, '_')
|
||||||
|
expect(normalized).toBe(serviceId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -3,8 +3,13 @@ import { member, permissionGroup, permissionGroupMember } from '@sim/db/schema'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import { isOrganizationOnEnterprisePlan } from '@/lib/billing'
|
import { isOrganizationOnEnterprisePlan } from '@/lib/billing'
|
||||||
import { isAccessControlEnabled, isHosted } from '@/lib/core/config/feature-flags'
|
|
||||||
import {
|
import {
|
||||||
|
getAllowedIntegrationsFromEnv,
|
||||||
|
isAccessControlEnabled,
|
||||||
|
isHosted,
|
||||||
|
} from '@/lib/core/config/feature-flags'
|
||||||
|
import {
|
||||||
|
DEFAULT_PERMISSION_GROUP_CONFIG,
|
||||||
type PermissionGroupConfig,
|
type PermissionGroupConfig,
|
||||||
parsePermissionGroupConfig,
|
parsePermissionGroupConfig,
|
||||||
} from '@/lib/permission-groups/types'
|
} from '@/lib/permission-groups/types'
|
||||||
@@ -23,8 +28,12 @@ export class ProviderNotAllowedError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class IntegrationNotAllowedError extends Error {
|
export class IntegrationNotAllowedError extends Error {
|
||||||
constructor(blockType: string) {
|
constructor(blockType: string, reason?: string) {
|
||||||
super(`Integration "${blockType}" is not allowed based on your permission group settings`)
|
super(
|
||||||
|
reason
|
||||||
|
? `Integration "${blockType}" is not allowed: ${reason}`
|
||||||
|
: `Integration "${blockType}" is not allowed based on your permission group settings`
|
||||||
|
)
|
||||||
this.name = 'IntegrationNotAllowedError'
|
this.name = 'IntegrationNotAllowedError'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,11 +66,38 @@ export class InvitationsNotAllowedError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges the env allowlist into a permission config.
|
||||||
|
* If `config` is null and no env allowlist is set, returns null.
|
||||||
|
* If `config` is null but env allowlist is set, returns a default config with only allowedIntegrations set.
|
||||||
|
* If both are set, intersects the two allowlists.
|
||||||
|
*/
|
||||||
|
function mergeEnvAllowlist(config: PermissionGroupConfig | null): PermissionGroupConfig | null {
|
||||||
|
const envAllowlist = getAllowedIntegrationsFromEnv()
|
||||||
|
|
||||||
|
if (envAllowlist === null) {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config === null) {
|
||||||
|
return { ...DEFAULT_PERMISSION_GROUP_CONFIG, allowedIntegrations: envAllowlist }
|
||||||
|
}
|
||||||
|
|
||||||
|
const merged =
|
||||||
|
config.allowedIntegrations === null
|
||||||
|
? envAllowlist
|
||||||
|
: config.allowedIntegrations
|
||||||
|
.map((i) => i.toLowerCase())
|
||||||
|
.filter((i) => envAllowlist.includes(i))
|
||||||
|
|
||||||
|
return { ...config, allowedIntegrations: merged }
|
||||||
|
}
|
||||||
|
|
||||||
export async function getUserPermissionConfig(
|
export async function getUserPermissionConfig(
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<PermissionGroupConfig | null> {
|
): Promise<PermissionGroupConfig | null> {
|
||||||
if (!isHosted && !isAccessControlEnabled) {
|
if (!isHosted && !isAccessControlEnabled) {
|
||||||
return null
|
return mergeEnvAllowlist(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [membership] = await db
|
const [membership] = await db
|
||||||
@@ -71,12 +107,12 @@ export async function getUserPermissionConfig(
|
|||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
if (!membership) {
|
if (!membership) {
|
||||||
return null
|
return mergeEnvAllowlist(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isEnterprise = await isOrganizationOnEnterprisePlan(membership.organizationId)
|
const isEnterprise = await isOrganizationOnEnterprisePlan(membership.organizationId)
|
||||||
if (!isEnterprise) {
|
if (!isEnterprise) {
|
||||||
return null
|
return mergeEnvAllowlist(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [groupMembership] = await db
|
const [groupMembership] = await db
|
||||||
@@ -92,10 +128,10 @@ export async function getUserPermissionConfig(
|
|||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
if (!groupMembership) {
|
if (!groupMembership) {
|
||||||
return null
|
return mergeEnvAllowlist(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsePermissionGroupConfig(groupMembership.config)
|
return mergeEnvAllowlist(parsePermissionGroupConfig(groupMembership.config))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPermissionConfig(
|
export async function getPermissionConfig(
|
||||||
@@ -152,19 +188,25 @@ export async function validateBlockType(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userId) {
|
const config = userId ? await getPermissionConfig(userId, ctx) : mergeEnvAllowlist(null)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = await getPermissionConfig(userId, ctx)
|
|
||||||
|
|
||||||
if (!config || config.allowedIntegrations === null) {
|
if (!config || config.allowedIntegrations === null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.allowedIntegrations.includes(blockType)) {
|
if (!config.allowedIntegrations.includes(blockType.toLowerCase())) {
|
||||||
logger.warn('Integration blocked by permission group', { userId, blockType })
|
const envAllowlist = getAllowedIntegrationsFromEnv()
|
||||||
throw new IntegrationNotAllowedError(blockType)
|
const blockedByEnv = envAllowlist !== null && !envAllowlist.includes(blockType.toLowerCase())
|
||||||
|
logger.warn(
|
||||||
|
blockedByEnv
|
||||||
|
? 'Integration blocked by env allowlist'
|
||||||
|
: 'Integration blocked by permission group',
|
||||||
|
{ userId, blockType }
|
||||||
|
)
|
||||||
|
throw new IntegrationNotAllowedError(
|
||||||
|
blockType,
|
||||||
|
blockedByEnv ? 'blocked by server ALLOWED_INTEGRATIONS policy' : undefined
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ vi.mock('@/lib/core/config/feature-flags', () => ({
|
|||||||
isDev: true,
|
isDev: true,
|
||||||
isTest: false,
|
isTest: false,
|
||||||
getCostMultiplier: vi.fn().mockReturnValue(1),
|
getCostMultiplier: vi.fn().mockReturnValue(1),
|
||||||
|
getAllowedIntegrationsFromEnv: vi.fn().mockReturnValue(null),
|
||||||
isEmailVerificationEnabled: false,
|
isEmailVerificationEnabled: false,
|
||||||
isBillingEnabled: false,
|
isBillingEnabled: false,
|
||||||
isOrganizationsEnabled: false,
|
isOrganizationsEnabled: false,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||||
import { isAccessControlEnabled, isHosted } from '@/lib/core/config/feature-flags'
|
import { isAccessControlEnabled, isHosted } from '@/lib/core/config/feature-flags'
|
||||||
import {
|
import {
|
||||||
@@ -21,12 +22,44 @@ export interface PermissionConfigResult {
|
|||||||
isInvitationsDisabled: boolean
|
isInvitationsDisabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AllowedIntegrationsResponse {
|
||||||
|
allowedIntegrations: string[] | null
|
||||||
|
}
|
||||||
|
|
||||||
|
function useAllowedIntegrationsFromEnv() {
|
||||||
|
return useQuery<AllowedIntegrationsResponse>({
|
||||||
|
queryKey: ['allowedIntegrations', 'env'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch('/api/settings/allowed-integrations')
|
||||||
|
if (!response.ok) return { allowedIntegrations: null }
|
||||||
|
return response.json()
|
||||||
|
},
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intersects two allowlists. If either is null (unrestricted), returns the other.
|
||||||
|
* If both are set, returns only items present in both.
|
||||||
|
*/
|
||||||
|
function intersectAllowlists(a: string[] | null, b: string[] | null): string[] | null {
|
||||||
|
if (a === null) return b
|
||||||
|
if (b === null) return a
|
||||||
|
return a.map((i) => i.toLowerCase()).filter((i) => b.includes(i))
|
||||||
|
}
|
||||||
|
|
||||||
export function usePermissionConfig(): PermissionConfigResult {
|
export function usePermissionConfig(): PermissionConfigResult {
|
||||||
const accessControlDisabled = !isHosted && !isAccessControlEnabled
|
const accessControlDisabled = !isHosted && !isAccessControlEnabled
|
||||||
const { data: organizationsData } = useOrganizations()
|
const { data: organizationsData } = useOrganizations()
|
||||||
const activeOrganization = organizationsData?.activeOrganization
|
const activeOrganization = organizationsData?.activeOrganization
|
||||||
|
|
||||||
const { data: permissionData, isLoading } = useUserPermissionConfig(activeOrganization?.id)
|
const { data: permissionData, isLoading: isPermissionLoading } = useUserPermissionConfig(
|
||||||
|
activeOrganization?.id
|
||||||
|
)
|
||||||
|
const { data: envAllowlistData, isLoading: isEnvAllowlistLoading } =
|
||||||
|
useAllowedIntegrationsFromEnv()
|
||||||
|
|
||||||
|
const isLoading = isPermissionLoading || isEnvAllowlistLoading
|
||||||
|
|
||||||
const config = useMemo(() => {
|
const config = useMemo(() => {
|
||||||
if (accessControlDisabled) {
|
if (accessControlDisabled) {
|
||||||
@@ -40,13 +73,18 @@ export function usePermissionConfig(): PermissionConfigResult {
|
|||||||
|
|
||||||
const isInPermissionGroup = !accessControlDisabled && !!permissionData?.permissionGroupId
|
const isInPermissionGroup = !accessControlDisabled && !!permissionData?.permissionGroupId
|
||||||
|
|
||||||
|
const mergedAllowedIntegrations = useMemo(() => {
|
||||||
|
const envAllowlist = envAllowlistData?.allowedIntegrations ?? null
|
||||||
|
return intersectAllowlists(config.allowedIntegrations, envAllowlist)
|
||||||
|
}, [config.allowedIntegrations, envAllowlistData])
|
||||||
|
|
||||||
const isBlockAllowed = useMemo(() => {
|
const isBlockAllowed = useMemo(() => {
|
||||||
return (blockType: string) => {
|
return (blockType: string) => {
|
||||||
if (blockType === 'start_trigger') return true
|
if (blockType === 'start_trigger') return true
|
||||||
if (config.allowedIntegrations === null) return true
|
if (mergedAllowedIntegrations === null) return true
|
||||||
return config.allowedIntegrations.includes(blockType)
|
return mergedAllowedIntegrations.includes(blockType.toLowerCase())
|
||||||
}
|
}
|
||||||
}, [config.allowedIntegrations])
|
}, [mergedAllowedIntegrations])
|
||||||
|
|
||||||
const isProviderAllowed = useMemo(() => {
|
const isProviderAllowed = useMemo(() => {
|
||||||
return (providerId: string) => {
|
return (providerId: string) => {
|
||||||
@@ -57,13 +95,14 @@ export function usePermissionConfig(): PermissionConfigResult {
|
|||||||
|
|
||||||
const filterBlocks = useMemo(() => {
|
const filterBlocks = useMemo(() => {
|
||||||
return <T extends { type: string }>(blocks: T[]): T[] => {
|
return <T extends { type: string }>(blocks: T[]): T[] => {
|
||||||
if (config.allowedIntegrations === null) return blocks
|
if (mergedAllowedIntegrations === null) return blocks
|
||||||
return blocks.filter(
|
return blocks.filter(
|
||||||
(block) =>
|
(block) =>
|
||||||
block.type === 'start_trigger' || config.allowedIntegrations!.includes(block.type)
|
block.type === 'start_trigger' ||
|
||||||
|
mergedAllowedIntegrations.includes(block.type.toLowerCase())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [config.allowedIntegrations])
|
}, [mergedAllowedIntegrations])
|
||||||
|
|
||||||
const filterProviders = useMemo(() => {
|
const filterProviders = useMemo(() => {
|
||||||
return (providerIds: string[]): string[] => {
|
return (providerIds: string[]): string[] => {
|
||||||
@@ -77,9 +116,14 @@ export function usePermissionConfig(): PermissionConfigResult {
|
|||||||
return featureFlagDisabled || config.disableInvitations
|
return featureFlagDisabled || config.disableInvitations
|
||||||
}, [config.disableInvitations])
|
}, [config.disableInvitations])
|
||||||
|
|
||||||
|
const mergedConfig = useMemo(
|
||||||
|
() => ({ ...config, allowedIntegrations: mergedAllowedIntegrations }),
|
||||||
|
[config, mergedAllowedIntegrations]
|
||||||
|
)
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
config,
|
config: mergedConfig,
|
||||||
isLoading,
|
isLoading,
|
||||||
isInPermissionGroup,
|
isInPermissionGroup,
|
||||||
filterBlocks,
|
filterBlocks,
|
||||||
@@ -89,7 +133,7 @@ export function usePermissionConfig(): PermissionConfigResult {
|
|||||||
isInvitationsDisabled,
|
isInvitationsDisabled,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
config,
|
mergedConfig,
|
||||||
isLoading,
|
isLoading,
|
||||||
isInPermissionGroup,
|
isInPermissionGroup,
|
||||||
filterBlocks,
|
filterBlocks,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { db } from '@sim/db'
|
|||||||
import { copilotChats, document, knowledgeBase, templates } from '@sim/db/schema'
|
import { copilotChats, document, knowledgeBase, templates } from '@sim/db/schema'
|
||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq, isNull } from 'drizzle-orm'
|
import { and, eq, isNull } from 'drizzle-orm'
|
||||||
|
import { getAllowedIntegrationsFromEnv } from '@/lib/core/config/feature-flags'
|
||||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||||
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
|
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
|
||||||
import { isHiddenFromDisplay } from '@/blocks/types'
|
import { isHiddenFromDisplay } from '@/blocks/types'
|
||||||
@@ -349,16 +350,14 @@ async function processBlockMetadata(
|
|||||||
userId?: string
|
userId?: string
|
||||||
): Promise<AgentContext | null> {
|
): Promise<AgentContext | null> {
|
||||||
try {
|
try {
|
||||||
if (userId) {
|
const permissionConfig = userId ? await getUserPermissionConfig(userId) : null
|
||||||
const permissionConfig = await getUserPermissionConfig(userId)
|
const allowedIntegrations =
|
||||||
const allowedIntegrations = permissionConfig?.allowedIntegrations
|
permissionConfig?.allowedIntegrations ?? getAllowedIntegrationsFromEnv()
|
||||||
if (allowedIntegrations != null && !allowedIntegrations.includes(blockId)) {
|
if (allowedIntegrations != null && !allowedIntegrations.includes(blockId.toLowerCase())) {
|
||||||
logger.debug('Block not allowed by permission group', { blockId, userId })
|
logger.debug('Block not allowed by integration allowlist', { blockId, userId })
|
||||||
return null
|
return null
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reuse registry to match get_blocks_metadata tool result
|
|
||||||
const { registry: blockRegistry } = await import('@/blocks/registry')
|
const { registry: blockRegistry } = await import('@/blocks/registry')
|
||||||
const { tools: toolsRegistry } = await import('@/tools/registry')
|
const { tools: toolsRegistry } = await import('@/tools/registry')
|
||||||
const SPECIAL_BLOCKS_METADATA: Record<string, any> = {}
|
const SPECIAL_BLOCKS_METADATA: Record<string, any> = {}
|
||||||
@@ -466,7 +465,6 @@ async function processWorkflowBlockFromDb(
|
|||||||
if (!block) return null
|
if (!block) return null
|
||||||
const tag = label ? `@${label} in Workflow` : `@${block.name || blockId} in Workflow`
|
const tag = label ? `@${label} in Workflow` : `@${block.name || blockId} in Workflow`
|
||||||
|
|
||||||
// Build content: isolate the block and include its subBlocks fully
|
|
||||||
const contentObj = {
|
const contentObj = {
|
||||||
workflowId,
|
workflowId,
|
||||||
block: block,
|
block: block,
|
||||||
@@ -518,7 +516,6 @@ async function processExecutionLogFromDb(
|
|||||||
endedAt: log.endedAt?.toISOString?.() || (log.endedAt ? String(log.endedAt) : null),
|
endedAt: log.endedAt?.toISOString?.() || (log.endedAt ? String(log.endedAt) : null),
|
||||||
totalDurationMs: log.totalDurationMs ?? null,
|
totalDurationMs: log.totalDurationMs ?? null,
|
||||||
workflowName: log.workflowName || '',
|
workflowName: log.workflowName || '',
|
||||||
// Include trace spans and any available details without being huge
|
|
||||||
executionData: log.executionData
|
executionData: log.executionData
|
||||||
? {
|
? {
|
||||||
traceSpans: (log.executionData as any).traceSpans || undefined,
|
traceSpans: (log.executionData as any).traceSpans || undefined,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
GetBlockConfigResult,
|
GetBlockConfigResult,
|
||||||
type GetBlockConfigResultType,
|
type GetBlockConfigResultType,
|
||||||
} from '@/lib/copilot/tools/shared/schemas'
|
} from '@/lib/copilot/tools/shared/schemas'
|
||||||
|
import { getAllowedIntegrationsFromEnv } from '@/lib/core/config/feature-flags'
|
||||||
import { registry as blockRegistry, getLatestBlock } from '@/blocks/registry'
|
import { registry as blockRegistry, getLatestBlock } from '@/blocks/registry'
|
||||||
import { isHiddenFromDisplay, type SubBlockConfig } from '@/blocks/types'
|
import { isHiddenFromDisplay, type SubBlockConfig } from '@/blocks/types'
|
||||||
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
||||||
@@ -439,9 +440,10 @@ export const getBlockConfigServerTool: BaseServerTool<
|
|||||||
}
|
}
|
||||||
|
|
||||||
const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null
|
const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null
|
||||||
const allowedIntegrations = permissionConfig?.allowedIntegrations
|
const allowedIntegrations =
|
||||||
|
permissionConfig?.allowedIntegrations ?? getAllowedIntegrationsFromEnv()
|
||||||
|
|
||||||
if (allowedIntegrations != null && !allowedIntegrations.includes(blockType)) {
|
if (allowedIntegrations != null && !allowedIntegrations.includes(blockType.toLowerCase())) {
|
||||||
throw new Error(`Block "${blockType}" is not available`)
|
throw new Error(`Block "${blockType}" is not available`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
GetBlockOptionsResult,
|
GetBlockOptionsResult,
|
||||||
type GetBlockOptionsResultType,
|
type GetBlockOptionsResultType,
|
||||||
} from '@/lib/copilot/tools/shared/schemas'
|
} from '@/lib/copilot/tools/shared/schemas'
|
||||||
|
import { getAllowedIntegrationsFromEnv } from '@/lib/core/config/feature-flags'
|
||||||
import { registry as blockRegistry, getLatestBlock } from '@/blocks/registry'
|
import { registry as blockRegistry, getLatestBlock } from '@/blocks/registry'
|
||||||
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
||||||
import { tools as toolsRegistry } from '@/tools/registry'
|
import { tools as toolsRegistry } from '@/tools/registry'
|
||||||
@@ -59,9 +60,10 @@ export const getBlockOptionsServerTool: BaseServerTool<
|
|||||||
}
|
}
|
||||||
|
|
||||||
const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null
|
const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null
|
||||||
const allowedIntegrations = permissionConfig?.allowedIntegrations
|
const allowedIntegrations =
|
||||||
|
permissionConfig?.allowedIntegrations ?? getAllowedIntegrationsFromEnv()
|
||||||
|
|
||||||
if (allowedIntegrations != null && !allowedIntegrations.includes(blockId)) {
|
if (allowedIntegrations != null && !allowedIntegrations.includes(blockId.toLowerCase())) {
|
||||||
throw new Error(`Block "${blockId}" is not available`)
|
throw new Error(`Block "${blockId}" is not available`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
|
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
|
||||||
import { GetBlocksAndToolsInput, GetBlocksAndToolsResult } from '@/lib/copilot/tools/shared/schemas'
|
import { GetBlocksAndToolsInput, GetBlocksAndToolsResult } from '@/lib/copilot/tools/shared/schemas'
|
||||||
|
import { getAllowedIntegrationsFromEnv } from '@/lib/core/config/feature-flags'
|
||||||
import { registry as blockRegistry } from '@/blocks/registry'
|
import { registry as blockRegistry } from '@/blocks/registry'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
||||||
@@ -17,7 +18,8 @@ export const getBlocksAndToolsServerTool: BaseServerTool<
|
|||||||
logger.debug('Executing get_blocks_and_tools')
|
logger.debug('Executing get_blocks_and_tools')
|
||||||
|
|
||||||
const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null
|
const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null
|
||||||
const allowedIntegrations = permissionConfig?.allowedIntegrations
|
const allowedIntegrations =
|
||||||
|
permissionConfig?.allowedIntegrations ?? getAllowedIntegrationsFromEnv()
|
||||||
|
|
||||||
type BlockListItem = {
|
type BlockListItem = {
|
||||||
type: string
|
type: string
|
||||||
@@ -30,7 +32,8 @@ export const getBlocksAndToolsServerTool: BaseServerTool<
|
|||||||
Object.entries(blockRegistry)
|
Object.entries(blockRegistry)
|
||||||
.filter(([blockType, blockConfig]: [string, BlockConfig]) => {
|
.filter(([blockType, blockConfig]: [string, BlockConfig]) => {
|
||||||
if (blockConfig.hideFromToolbar) return false
|
if (blockConfig.hideFromToolbar) return false
|
||||||
if (allowedIntegrations != null && !allowedIntegrations.includes(blockType)) return false
|
if (allowedIntegrations != null && !allowedIntegrations.includes(blockType.toLowerCase()))
|
||||||
|
return false
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
.forEach(([blockType, blockConfig]: [string, BlockConfig]) => {
|
.forEach(([blockType, blockConfig]: [string, BlockConfig]) => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { join } from 'path'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
|
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
|
||||||
import { GetBlocksMetadataInput, GetBlocksMetadataResult } from '@/lib/copilot/tools/shared/schemas'
|
import { GetBlocksMetadataInput, GetBlocksMetadataResult } from '@/lib/copilot/tools/shared/schemas'
|
||||||
|
import { getAllowedIntegrationsFromEnv } from '@/lib/core/config/feature-flags'
|
||||||
import { registry as blockRegistry } from '@/blocks/registry'
|
import { registry as blockRegistry } from '@/blocks/registry'
|
||||||
import { AuthMode, type BlockConfig, isHiddenFromDisplay } from '@/blocks/types'
|
import { AuthMode, type BlockConfig, isHiddenFromDisplay } from '@/blocks/types'
|
||||||
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
||||||
@@ -112,11 +113,12 @@ export const getBlocksMetadataServerTool: BaseServerTool<
|
|||||||
logger.debug('Executing get_blocks_metadata', { count: blockIds?.length })
|
logger.debug('Executing get_blocks_metadata', { count: blockIds?.length })
|
||||||
|
|
||||||
const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null
|
const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null
|
||||||
const allowedIntegrations = permissionConfig?.allowedIntegrations
|
const allowedIntegrations =
|
||||||
|
permissionConfig?.allowedIntegrations ?? getAllowedIntegrationsFromEnv()
|
||||||
|
|
||||||
const result: Record<string, CopilotBlockMetadata> = {}
|
const result: Record<string, CopilotBlockMetadata> = {}
|
||||||
for (const blockId of blockIds || []) {
|
for (const blockId of blockIds || []) {
|
||||||
if (allowedIntegrations != null && !allowedIntegrations.includes(blockId)) {
|
if (allowedIntegrations != null && !allowedIntegrations.includes(blockId.toLowerCase())) {
|
||||||
logger.debug('Block not allowed by permission group', { blockId })
|
logger.debug('Block not allowed by permission group', { blockId })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -420,7 +422,6 @@ function extractInputs(metadata: CopilotBlockMetadata): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (schema.options && schema.options.length > 0) {
|
if (schema.options && schema.options.length > 0) {
|
||||||
// Always return the id (actual value to use), not the display label
|
|
||||||
input.options = schema.options.map((opt) => opt.id || opt.label)
|
input.options = schema.options.map((opt) => opt.id || opt.label)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
|
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
|
||||||
|
import { getAllowedIntegrationsFromEnv } from '@/lib/core/config/feature-flags'
|
||||||
import { registry as blockRegistry } from '@/blocks/registry'
|
import { registry as blockRegistry } from '@/blocks/registry'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
||||||
@@ -22,13 +23,15 @@ export const getTriggerBlocksServerTool: BaseServerTool<
|
|||||||
logger.debug('Executing get_trigger_blocks')
|
logger.debug('Executing get_trigger_blocks')
|
||||||
|
|
||||||
const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null
|
const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null
|
||||||
const allowedIntegrations = permissionConfig?.allowedIntegrations
|
const allowedIntegrations =
|
||||||
|
permissionConfig?.allowedIntegrations ?? getAllowedIntegrationsFromEnv()
|
||||||
|
|
||||||
const triggerBlockIds: string[] = []
|
const triggerBlockIds: string[] = []
|
||||||
|
|
||||||
Object.entries(blockRegistry).forEach(([blockType, blockConfig]: [string, BlockConfig]) => {
|
Object.entries(blockRegistry).forEach(([blockType, blockConfig]: [string, BlockConfig]) => {
|
||||||
if (blockConfig.hideFromToolbar) return
|
if (blockConfig.hideFromToolbar) return
|
||||||
if (allowedIntegrations != null && !allowedIntegrations.includes(blockType)) return
|
if (allowedIntegrations != null && !allowedIntegrations.includes(blockType.toLowerCase()))
|
||||||
|
return
|
||||||
|
|
||||||
if (blockConfig.category === 'triggers') {
|
if (blockConfig.category === 'triggers') {
|
||||||
triggerBlockIds.push(blockType)
|
triggerBlockIds.push(blockType)
|
||||||
|
|||||||
@@ -657,7 +657,7 @@ export function isBlockTypeAllowed(
|
|||||||
if (!permissionConfig || permissionConfig.allowedIntegrations === null) {
|
if (!permissionConfig || permissionConfig.allowedIntegrations === null) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return permissionConfig.allowedIntegrations.includes(blockType)
|
return permissionConfig.allowedIntegrations.includes(blockType.toLowerCase())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export const env = createEnv({
|
|||||||
BLACKLISTED_PROVIDERS: z.string().optional(), // Comma-separated provider IDs to hide (e.g., "openai,anthropic")
|
BLACKLISTED_PROVIDERS: z.string().optional(), // Comma-separated provider IDs to hide (e.g., "openai,anthropic")
|
||||||
BLACKLISTED_MODELS: z.string().optional(), // Comma-separated model names/prefixes to hide (e.g., "gpt-4,claude-*")
|
BLACKLISTED_MODELS: z.string().optional(), // Comma-separated model names/prefixes to hide (e.g., "gpt-4,claude-*")
|
||||||
ALLOWED_MCP_DOMAINS: z.string().optional(), // Comma-separated domains for MCP servers (e.g., "internal.company.com,mcp.example.org"). Empty = all allowed.
|
ALLOWED_MCP_DOMAINS: z.string().optional(), // Comma-separated domains for MCP servers (e.g., "internal.company.com,mcp.example.org"). Empty = all allowed.
|
||||||
|
ALLOWED_INTEGRATIONS: z.string().optional(), // Comma-separated block types to allow (e.g., "slack,github,agent"). Empty = all allowed.
|
||||||
|
|
||||||
// Azure Configuration - Shared credentials with feature-specific models
|
// Azure Configuration - Shared credentials with feature-specific models
|
||||||
AZURE_OPENAI_ENDPOINT: z.string().url().optional(), // Shared Azure OpenAI service endpoint
|
AZURE_OPENAI_ENDPOINT: z.string().url().optional(), // Shared Azure OpenAI service endpoint
|
||||||
|
|||||||
@@ -123,6 +123,18 @@ export const isReactGrabEnabled = isDev && isTruthy(env.REACT_GRAB_ENABLED)
|
|||||||
*/
|
*/
|
||||||
export const isReactScanEnabled = isDev && isTruthy(env.REACT_SCAN_ENABLED)
|
export const isReactScanEnabled = isDev && isTruthy(env.REACT_SCAN_ENABLED)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the parsed allowlist of integration block types from the environment variable.
|
||||||
|
* If not set or empty, returns null (meaning all integrations are allowed).
|
||||||
|
*/
|
||||||
|
export function getAllowedIntegrationsFromEnv(): string[] | null {
|
||||||
|
if (!env.ALLOWED_INTEGRATIONS) return null
|
||||||
|
const parsed = env.ALLOWED_INTEGRATIONS.split(',')
|
||||||
|
.map((i) => i.trim().toLowerCase())
|
||||||
|
.filter(Boolean)
|
||||||
|
return parsed.length > 0 ? parsed : null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalizes a domain entry from the ALLOWED_MCP_DOMAINS env var.
|
* Normalizes a domain entry from the ALLOWED_MCP_DOMAINS env var.
|
||||||
* Accepts bare hostnames (e.g., "mcp.company.com") or full URLs (e.g., "https://mcp.company.com").
|
* Accepts bare hostnames (e.g., "mcp.company.com") or full URLs (e.g., "https://mcp.company.com").
|
||||||
|
|||||||
@@ -195,6 +195,9 @@ app:
|
|||||||
BLACKLISTED_MODELS: "" # Comma-separated model names/prefixes to hide (e.g., "gpt-4,claude-*")
|
BLACKLISTED_MODELS: "" # Comma-separated model names/prefixes to hide (e.g., "gpt-4,claude-*")
|
||||||
ALLOWED_MCP_DOMAINS: "" # Comma-separated domains for MCP servers (e.g., "internal.company.com,mcp.example.org"). Empty = all allowed.
|
ALLOWED_MCP_DOMAINS: "" # Comma-separated domains for MCP servers (e.g., "internal.company.com,mcp.example.org"). Empty = all allowed.
|
||||||
|
|
||||||
|
# Integration/Block Restrictions (leave empty if not restricting)
|
||||||
|
ALLOWED_INTEGRATIONS: "" # Comma-separated block types to allow (e.g., "slack,github,agent"). Empty = all allowed.
|
||||||
|
|
||||||
# Invitation Control
|
# Invitation Control
|
||||||
DISABLE_INVITATIONS: "" # Set to "true" to disable workspace invitations globally
|
DISABLE_INVITATIONS: "" # Set to "true" to disable workspace invitations globally
|
||||||
NEXT_PUBLIC_DISABLE_INVITATIONS: "" # Set to "true" to hide invitation UI elements
|
NEXT_PUBLIC_DISABLE_INVITATIONS: "" # Set to "true" to hide invitation UI elements
|
||||||
|
|||||||
Reference in New Issue
Block a user