diff --git a/apps/sim/app/api/guardrails/validate/route.ts b/apps/sim/app/api/guardrails/validate/route.ts index e9d19853c04..03bf505a4df 100644 --- a/apps/sim/app/api/guardrails/validate/route.ts +++ b/apps/sim/app/api/guardrails/validate/route.ts @@ -12,6 +12,7 @@ import { validatePII } from '@/lib/guardrails/validate_pii' import { validateRegex } from '@/lib/guardrails/validate_regex' import { assertPermissionsAllowed, + ModelNotAllowedError, ProviderNotAllowedError, } from '@/ee/access-control/utils/permission-check' @@ -161,7 +162,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { model, }) } catch (err) { - if (err instanceof ProviderNotAllowedError) { + if (err instanceof ProviderNotAllowedError || err instanceof ModelNotAllowedError) { return NextResponse.json({ success: true, output: { diff --git a/apps/sim/app/api/providers/route.ts b/apps/sim/app/api/providers/route.ts index f0bfc2b4a45..bc5e344772b 100644 --- a/apps/sim/app/api/providers/route.ts +++ b/apps/sim/app/api/providers/route.ts @@ -19,6 +19,7 @@ import { import { assertPermissionsAllowed, IntegrationNotAllowedError, + ModelNotAllowedError, ProviderNotAllowedError, } from '@/ee/access-control/utils/permission-check' import type { StreamingExecution } from '@/executor/types' @@ -132,7 +133,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => { model, }) } catch (err) { - if (err instanceof ProviderNotAllowedError || err instanceof IntegrationNotAllowedError) { + if ( + err instanceof ProviderNotAllowedError || + err instanceof ModelNotAllowedError || + err instanceof IntegrationNotAllowedError + ) { return NextResponse.json({ error: err.message }, { status: 403 }) } throw err diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx index e802667aa4b..d5437cf025b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx @@ -151,7 +151,11 @@ export const ComboBox = memo(function ComboBox({ const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue // Permission-based filtering for model dropdowns - const { isProviderAllowed, isLoading: isPermissionLoading } = usePermissionConfig() + const { + isProviderAllowed, + isModelAllowed, + isLoading: isPermissionLoading, + } = usePermissionConfig() // Evaluate static options if provided as a function const staticOptions = useMemo(() => { @@ -160,9 +164,9 @@ export const ComboBox = memo(function ComboBox({ if (subBlockId === 'model') { return opts.filter((opt) => { const modelId = typeof opt === 'string' ? opt : opt.id + if (!isModelAllowed(modelId)) return false try { - const providerId = getProviderFromModel(modelId) - return isProviderAllowed(providerId) + return isProviderAllowed(getProviderFromModel(modelId)) } catch { return true } @@ -170,7 +174,7 @@ export const ComboBox = memo(function ComboBox({ } return opts - }, [options, subBlockId, isProviderAllowed]) + }, [options, subBlockId, isProviderAllowed, isModelAllowed]) // Normalize fetched options to match ComboBoxOption format const normalizedFetchedOptions = useMemo((): ComboBoxOption[] => { @@ -185,9 +189,9 @@ export const ComboBox = memo(function ComboBox({ if (subBlockId === 'model' && fetchOptions && normalizedFetchedOptions.length > 0) { opts = opts.filter((opt) => { const modelId = typeof opt === 'string' ? opt : opt.id + if (!isModelAllowed(modelId)) return false try { - const providerId = getProviderFromModel(modelId) - return isProviderAllowed(providerId) + return isProviderAllowed(getProviderFromModel(modelId)) } catch { return true } @@ -212,6 +216,7 @@ export const ComboBox = memo(function ComboBox({ hydratedOption, subBlockId, isProviderAllowed, + isModelAllowed, ]) // Convert options to Combobox format diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index 930c9da176b..30f744225d6 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from 'react' import { createLogger } from '@sim/logger' -import { Plus, Search } from 'lucide-react' +import { ChevronDown, Plus, Search } from 'lucide-react' import { useParams } from 'next/navigation' import { Avatar, @@ -27,6 +27,7 @@ import { } from '@/components/emcn' import { Input as BaseInput } from '@/components/ui' import { getEnv, isTruthy } from '@/lib/core/config/env' +import { cn } from '@/lib/core/utils/cn' import type { PermissionGroupConfig } from '@/lib/permission-groups/types' import { getUserColor } from '@/lib/workspaces/colors' import { getAllBlocks } from '@/blocks' @@ -42,9 +43,16 @@ import { useUserPermissionConfig, } from '@/ee/access-control/hooks/permission-groups' import { useBlacklistedProviders } from '@/hooks/queries/allowed-providers' +import { useProviderModels } from '@/hooks/queries/providers' import { useWorkspacePermissionsQuery } from '@/hooks/queries/workspace' -import { PROVIDER_DEFINITIONS } from '@/providers/models' -import { getAllProviderIds } from '@/providers/utils' +import { + DYNAMIC_MODEL_PROVIDERS, + getProviderModels, + PROVIDER_DEFINITIONS, +} from '@/providers/models' +import type { ProviderId } from '@/providers/types' +import { getAllProviderIds, getProviderFromModel } from '@/providers/utils' +import type { ProviderName } from '@/stores/providers' const logger = createLogger('AccessControl') @@ -252,6 +260,188 @@ function AccessControlSkeleton() { ) } +interface ModelDenylistControls { + isModelAllowed: (model: string) => boolean + onToggleModel: (model: string) => void + onSetModelsDenied: (models: string[], denied: boolean) => void +} + +interface ModelCheckboxGridProps extends ModelDenylistControls { + models: string[] + isLoading: boolean +} + +function ModelCheckboxGrid({ + models, + isLoading, + isModelAllowed, + onToggleModel, + onSetModelsDenied, +}: ModelCheckboxGridProps) { + const [search, setSearch] = useState('') + + const sortedModels = useMemo(() => [...models].sort((a, b) => a.localeCompare(b)), [models]) + + const filteredModels = useMemo(() => { + if (!search.trim()) return sortedModels + const query = search.toLowerCase() + return sortedModels.filter((model) => model.toLowerCase().includes(query)) + }, [sortedModels, search]) + + if (isLoading) { + return