Extend access groups to passthrough routes and vector stores

Adds two resource lists to the unified access group table,
access_passthrough_routes and access_vector_store_ids, so a single
access group can grant passthrough routes and vector stores alongside
models, MCP servers, and agents.

Vector store enforcement in vector_store_access_check now unions the
stores granted through a key's or team's access groups with the
existing object_permission.vector_stores list. Passthrough enforcement
resolves access-group routes into access_group_passthrough_routes during
common_checks so the synchronous route checks pick them up next to the
existing allowed_passthrough_routes metadata; routes match by path with
the same exact/prefix semantics.

UI: the access group editor gains pass through routes and vector store
selectors, and the details page shows both resource lists.
This commit is contained in:
ryan-crabbe-berri 2026-06-05 12:29:45 -07:00
parent 4a5644d51e
commit c992955648
22 changed files with 435 additions and 22 deletions

View file

@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "LiteLLM_AccessGroupTable" ADD COLUMN "access_passthrough_routes" TEXT[] DEFAULT ARRAY[]::TEXT[],
ADD COLUMN "access_vector_store_ids" TEXT[] DEFAULT ARRAY[]::TEXT[];

View file

@ -1211,6 +1211,8 @@ model LiteLLM_AccessGroupTable {
access_model_names String[] @default([])
access_mcp_server_ids String[] @default([])
access_agent_ids String[] @default([])
access_passthrough_routes String[] @default([])
access_vector_store_ids String[] @default([])
assigned_team_ids String[] @default([])
assigned_key_ids String[] @default([])

View file

@ -2739,6 +2739,9 @@ class UserAPIKeyAuth(
# Decoded upstream IdP claims (groups, roles, etc.) propagated by JWT auth machinery
# and forwarded into outbound tokens by guardrails such as MCPJWTSigner.
jwt_claims: Optional[Dict] = None
# Passthrough routes granted to this token through its (and its team's) access
# groups, resolved in common_checks so the synchronous route checks can read them.
access_group_passthrough_routes: Optional[List[str]] = None
model_config = ConfigDict(arbitrary_types_allowed=True)
@ -3196,6 +3199,8 @@ class LiteLLM_AccessGroupTable(LiteLLMPydanticObjectBase):
access_model_names: List[str] = []
access_mcp_server_ids: List[str] = []
access_agent_ids: List[str] = []
access_passthrough_routes: List[str] = []
access_vector_store_ids: List[str] = []
assigned_team_ids: List[str] = []
assigned_key_ids: List[str] = []
created_at: Optional[datetime] = None

View file

@ -746,6 +746,19 @@ async def common_checks( # noqa: PLR0915
user_object=user_object, route=route, request_body=request_body
)
passthrough_access_group_ids = list(
{
*(valid_token.access_group_ids or []),
*((team_object.access_group_ids or []) if team_object is not None else []),
}
)
if passthrough_access_group_ids:
valid_token.access_group_passthrough_routes = (
await _get_passthrough_routes_from_access_groups(
access_group_ids=passthrough_access_group_ids,
)
)
_is_route_allowed = _is_api_route_allowed(
route=route,
request=request,
@ -2816,7 +2829,11 @@ async def get_org_object(
async def _get_resources_from_access_groups(
access_group_ids: List[str],
resource_field: Literal[
"access_model_names", "access_mcp_server_ids", "access_agent_ids"
"access_model_names",
"access_mcp_server_ids",
"access_agent_ids",
"access_passthrough_routes",
"access_vector_store_ids",
],
prisma_client: Optional[PrismaClient] = None,
user_api_key_cache: Optional[UserApiKeyCache] = None,
@ -2832,6 +2849,8 @@ async def _get_resources_from_access_groups(
- "access_model_names": model names (for model access checks)
- "access_mcp_server_ids": MCP server IDs (for MCP access checks)
- "access_agent_ids": agent IDs (for agent access checks)
- "access_passthrough_routes": passthrough route paths (for passthrough access checks)
- "access_vector_store_ids": vector store IDs (for vector store access checks)
prisma_client: Optional PrismaClient (lazy-imported from proxy_server if None)
user_api_key_cache: Optional DualCache (lazy-imported from proxy_server if None)
proxy_logging_obj: Optional ProxyLogging (lazy-imported from proxy_server if None)
@ -2931,6 +2950,44 @@ async def _get_agent_ids_from_access_groups(
)
async def _get_passthrough_routes_from_access_groups(
access_group_ids: List[str],
prisma_client: Optional[PrismaClient] = None,
user_api_key_cache: Optional[UserApiKeyCache] = None,
proxy_logging_obj: Optional[ProxyLogging] = None,
) -> List[str]:
"""
Collect passthrough route paths from unified access groups.
Routes are matched by path (exact or prefix), same as allowed_passthrough_routes.
"""
return await _get_resources_from_access_groups(
access_group_ids=access_group_ids,
resource_field="access_passthrough_routes",
prisma_client=prisma_client,
user_api_key_cache=user_api_key_cache,
proxy_logging_obj=proxy_logging_obj,
)
async def _get_vector_store_ids_from_access_groups(
access_group_ids: List[str],
prisma_client: Optional[PrismaClient] = None,
user_api_key_cache: Optional[UserApiKeyCache] = None,
proxy_logging_obj: Optional[ProxyLogging] = None,
) -> List[str]:
"""
Collect vector store IDs from unified access groups.
Vector stores are matched by vector_store_id.
"""
return await _get_resources_from_access_groups(
access_group_ids=access_group_ids,
resource_field="access_vector_store_ids",
prisma_client=prisma_client,
user_api_key_cache=user_api_key_cache,
proxy_logging_obj=proxy_logging_obj,
)
def _check_model_access_helper(
model: str,
llm_router: Optional[Router],
@ -4490,6 +4547,9 @@ async def vector_store_access_check(
object_type="key",
vector_store_ids_to_run=vector_store_ids_to_run,
object_permissions=key_object_permission,
access_group_vector_store_ids=await _get_vector_store_ids_from_access_groups(
access_group_ids=valid_token.access_group_ids or [],
),
)
# Check if the team can access the vector store
@ -4504,6 +4564,9 @@ async def vector_store_access_check(
object_type="team",
vector_store_ids_to_run=vector_store_ids_to_run,
object_permissions=team_object_permission,
access_group_vector_store_ids=await _get_vector_store_ids_from_access_groups(
access_group_ids=team_object.access_group_ids or [],
),
)
return True
@ -4512,9 +4575,13 @@ def _can_object_call_vector_stores(
object_type: Literal["key", "team", "org"],
vector_store_ids_to_run: List[str],
object_permissions: Optional[LiteLLM_ObjectPermissionTable],
access_group_vector_store_ids: Optional[List[str]] = None,
):
"""
Raises ProxyException if the object (key, team, org) cannot access the specific vector store.
Access is granted if a requested vector store is in the object's object_permission
list OR in a vector store granted through one of the object's access groups.
"""
if object_permissions is None:
return True
@ -4526,10 +4593,14 @@ def _can_object_call_vector_stores(
if len(object_permissions.vector_stores) == 0:
return True
allowed_vector_store_ids = set(object_permissions.vector_stores) | set(
access_group_vector_store_ids or []
)
for vector_store_id in vector_store_ids_to_run:
if vector_store_id not in object_permissions.vector_stores:
if vector_store_id not in allowed_vector_store_ids:
raise ProxyException(
message=f"User not allowed to access vector store. Tried to access {vector_store_id}. Only allowed to access {object_permissions.vector_stores}",
message=f"User not allowed to access vector store. Tried to access {vector_store_id}. Only allowed to access {sorted(allowed_vector_store_ids)}",
type=ProxyErrorTypes.get_vector_store_access_error_type_for_object(
object_type
),

View file

@ -730,27 +730,19 @@ class RouteChecks:
"""
Check if route is a passthrough route.
Supports both exact match and prefix match.
Allowed routes come from `allowed_passthrough_routes` on the key or team
metadata, plus any routes granted through the key's or team's access groups
(resolved into `access_group_passthrough_routes` during common_checks).
"""
metadata = user_api_key_dict.metadata
metadata = user_api_key_dict.metadata or {}
team_metadata = user_api_key_dict.team_metadata or {}
if metadata is None and team_metadata is None:
return False
if (
"allowed_passthrough_routes" not in metadata
and "allowed_passthrough_routes" not in team_metadata
):
return False
if (
metadata.get("allowed_passthrough_routes") is None
and team_metadata.get("allowed_passthrough_routes") is None
):
return False
allowed_passthrough_routes = (
metadata.get("allowed_passthrough_routes")
or team_metadata.get("allowed_passthrough_routes")
or []
)
) + (user_api_key_dict.access_group_passthrough_routes or [])
# Check if route matches any allowed passthrough route (exact or prefix match)
for allowed_route in allowed_passthrough_routes:

View file

@ -57,6 +57,8 @@ def _record_to_response(record) -> AccessGroupResponse:
access_model_names=record.access_model_names,
access_mcp_server_ids=record.access_mcp_server_ids,
access_agent_ids=record.access_agent_ids,
access_passthrough_routes=record.access_passthrough_routes,
access_vector_store_ids=record.access_vector_store_ids,
assigned_team_ids=record.assigned_team_ids,
assigned_key_ids=record.assigned_key_ids,
created_at=record.created_at,
@ -330,6 +332,8 @@ async def create_access_group(
"access_model_names": data.access_model_names or [],
"access_mcp_server_ids": data.access_mcp_server_ids or [],
"access_agent_ids": data.access_agent_ids or [],
"access_passthrough_routes": data.access_passthrough_routes or [],
"access_vector_store_ids": data.access_vector_store_ids or [],
"assigned_team_ids": data.assigned_team_ids or [],
"assigned_key_ids": data.assigned_key_ids or [],
"created_by": user_api_key_dict.user_id,
@ -441,6 +445,8 @@ async def update_access_group(
"access_model_names",
"access_mcp_server_ids",
"access_agent_ids",
"access_passthrough_routes",
"access_vector_store_ids",
)
and value is None
):

View file

@ -1211,6 +1211,8 @@ model LiteLLM_AccessGroupTable {
access_model_names String[] @default([])
access_mcp_server_ids String[] @default([])
access_agent_ids String[] @default([])
access_passthrough_routes String[] @default([])
access_vector_store_ids String[] @default([])
assigned_team_ids String[] @default([])
assigned_key_ids String[] @default([])

View file

@ -10,6 +10,8 @@ class AccessGroupCreateRequest(BaseModel):
access_model_names: Optional[List[str]] = None
access_mcp_server_ids: Optional[List[str]] = None
access_agent_ids: Optional[List[str]] = None
access_passthrough_routes: Optional[List[str]] = None
access_vector_store_ids: Optional[List[str]] = None
assigned_team_ids: Optional[List[str]] = None
assigned_key_ids: Optional[List[str]] = None
@ -20,6 +22,8 @@ class AccessGroupUpdateRequest(BaseModel):
access_model_names: Optional[List[str]] = None
access_mcp_server_ids: Optional[List[str]] = None
access_agent_ids: Optional[List[str]] = None
access_passthrough_routes: Optional[List[str]] = None
access_vector_store_ids: Optional[List[str]] = None
assigned_team_ids: Optional[List[str]] = None
assigned_key_ids: Optional[List[str]] = None
@ -31,6 +35,8 @@ class AccessGroupResponse(BaseModel):
access_model_names: List[str]
access_mcp_server_ids: List[str]
access_agent_ids: List[str]
access_passthrough_routes: List[str]
access_vector_store_ids: List[str]
assigned_team_ids: List[str]
assigned_key_ids: List[str]
created_at: datetime

View file

@ -1211,6 +1211,8 @@ model LiteLLM_AccessGroupTable {
access_model_names String[] @default([])
access_mcp_server_ids String[] @default([])
access_agent_ids String[] @default([])
access_passthrough_routes String[] @default([])
access_vector_store_ids String[] @default([])
assigned_team_ids String[] @default([])
assigned_key_ids String[] @default([])

View file

@ -879,6 +879,87 @@ async def test_vector_store_access_check_with_team_permissions():
assert exc_info.value.type == ProxyErrorTypes.team_vector_store_access_denied
def test_can_object_call_vector_stores_access_group_widens():
"""A vector store granted through an access group is allowed even when it is
not in the object's object_permission list."""
mock_permissions = MagicMock()
mock_permissions.vector_stores = ["store-1"]
# store-2 is not in object_permission but is granted via an access group
result = _can_object_call_vector_stores(
object_type="key",
vector_store_ids_to_run=["store-2"],
object_permissions=mock_permissions,
access_group_vector_store_ids=["store-2"],
)
assert result is True
# store-2 in neither object_permission nor access group -> denied
with pytest.raises(ProxyException) as exc_info:
_can_object_call_vector_stores(
object_type="key",
vector_store_ids_to_run=["store-2"],
object_permissions=mock_permissions,
access_group_vector_store_ids=["store-9"],
)
assert exc_info.value.type == ProxyErrorTypes.key_vector_store_access_denied
@pytest.mark.asyncio
async def test_vector_store_access_check_access_group_grant():
"""vector_store_access_check unions access-group-granted stores with the key's
object_permission, so an access-group grant unblocks an otherwise-denied store."""
request_body = {"tools": [{"type": "function", "function": {"name": "test"}}]}
valid_token = UserAPIKeyAuth(
token="ag-test-token",
object_permission_id="perm-123",
access_group_ids=["ag-1"],
)
mock_prisma_client = MagicMock()
mock_permissions = MagicMock()
mock_permissions.vector_stores = ["store-1"]
mock_prisma_client.db.litellm_objectpermissiontable.find_unique = AsyncMock(
return_value=mock_permissions
)
mock_vector_store_registry = MagicMock()
mock_vector_store_registry.get_vector_store_ids_to_run.return_value = ["store-2"]
# Access group grants store-2 -> request for store-2 is allowed
with (
patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client),
patch("litellm.vector_store_registry", mock_vector_store_registry),
patch(
"litellm.proxy.auth.auth_checks._get_vector_store_ids_from_access_groups",
AsyncMock(return_value=["store-2"]),
),
):
result = await vector_store_access_check(
request_body=request_body,
team_object=None,
valid_token=valid_token,
)
assert result is True
# Access group grants nothing -> request for store-2 is denied
with (
patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client),
patch("litellm.vector_store_registry", mock_vector_store_registry),
patch(
"litellm.proxy.auth.auth_checks._get_vector_store_ids_from_access_groups",
AsyncMock(return_value=[]),
),
):
with pytest.raises(ProxyException) as exc_info:
await vector_store_access_check(
request_body=request_body,
team_object=None,
valid_token=valid_token,
)
assert exc_info.value.type == ProxyErrorTypes.key_vector_store_access_denied
def test_can_object_call_model_with_alias():
"""Test that can_object_call_model works with model aliases"""
from litellm import Router

View file

@ -1321,6 +1321,63 @@ def test_check_passthrough_route_access_empty_list():
assert result is False
def test_check_passthrough_route_access_access_group_route():
"""A passthrough route granted through an access group (resolved into
access_group_passthrough_routes) is allowed even with no metadata routes."""
valid_token = UserAPIKeyAuth(
user_id="test_user",
metadata={},
access_group_passthrough_routes=["/group-endpoint"],
)
# exact match via access group
assert (
RouteChecks.check_passthrough_route_access(
route="/group-endpoint",
user_api_key_dict=valid_token,
)
is True
)
# prefix match via access group
assert (
RouteChecks.check_passthrough_route_access(
route="/group-endpoint/v1/chat/completions",
user_api_key_dict=valid_token,
)
is True
)
# route not granted by the access group is denied
assert (
RouteChecks.check_passthrough_route_access(
route="/other-endpoint",
user_api_key_dict=valid_token,
)
is False
)
def test_check_passthrough_route_access_access_group_unions_with_metadata():
"""Access-group routes are additive to metadata routes; both sources grant access."""
valid_token = UserAPIKeyAuth(
user_id="test_user",
metadata={"allowed_passthrough_routes": ["/key-endpoint"]},
access_group_passthrough_routes=["/group-endpoint"],
)
assert (
RouteChecks.check_passthrough_route_access(
route="/key-endpoint", user_api_key_dict=valid_token
)
is True
)
assert (
RouteChecks.check_passthrough_route_access(
route="/group-endpoint", user_api_key_dict=valid_token
)
is True
)
@pytest.mark.parametrize(
"route",
[

View file

@ -31,6 +31,8 @@ def _make_access_group_record(
access_model_names: list | None = None,
access_mcp_server_ids: list | None = None,
access_agent_ids: list | None = None,
access_passthrough_routes: list | None = None,
access_vector_store_ids: list | None = None,
assigned_team_ids: list | None = None,
assigned_key_ids: list | None = None,
created_by: str | None = "admin-user",
@ -46,6 +48,8 @@ def _make_access_group_record(
"access_model_names": access_model_names or [],
"access_mcp_server_ids": access_mcp_server_ids or [],
"access_agent_ids": access_agent_ids or [],
"access_passthrough_routes": access_passthrough_routes or [],
"access_vector_store_ids": access_vector_store_ids or [],
"assigned_team_ids": assigned_team_ids or [],
"assigned_key_ids": assigned_key_ids or [],
"created_at": created_at_val,
@ -75,6 +79,8 @@ def client_and_mocks(monkeypatch):
access_model_names=data.get("access_model_names", []),
access_mcp_server_ids=data.get("access_mcp_server_ids", []),
access_agent_ids=data.get("access_agent_ids", []),
access_passthrough_routes=data.get("access_passthrough_routes", []),
access_vector_store_ids=data.get("access_vector_store_ids", []),
assigned_team_ids=data.get("assigned_team_ids", []),
assigned_key_ids=data.get("assigned_key_ids", []),
created_by=data.get("created_by"),
@ -92,6 +98,8 @@ def client_and_mocks(monkeypatch):
access_model_names=data.get("access_model_names", []),
access_mcp_server_ids=data.get("access_mcp_server_ids", []),
access_agent_ids=data.get("access_agent_ids", []),
access_passthrough_routes=data.get("access_passthrough_routes", []),
access_vector_store_ids=data.get("access_vector_store_ids", []),
assigned_team_ids=data.get("assigned_team_ids", []),
assigned_key_ids=data.get("assigned_key_ids", []),
updated_by=data.get("updated_by"),
@ -198,6 +206,48 @@ def test_create_access_group_success(client_and_mocks, base_path, payload):
mock_table.create.assert_awaited_once()
def test_create_access_group_persists_passthrough_and_vector_store_fields(
client_and_mocks,
):
"""Passthrough routes and vector store ids round-trip through create."""
client, _, mock_table, *_ = client_and_mocks
payload = {
"access_group_name": "group-resources",
"access_passthrough_routes": ["/bedrock", "/vllm"],
"access_vector_store_ids": ["vs-1", "vs-2"],
}
resp = client.post("/v1/access_group", json=payload)
assert resp.status_code == 201
body = resp.json()
assert body["access_passthrough_routes"] == ["/bedrock", "/vllm"]
assert body["access_vector_store_ids"] == ["vs-1", "vs-2"]
# the new fields are written to the DB row, not silently dropped
_, create_kwargs = mock_table.create.call_args
create_data = create_kwargs["data"]
assert create_data["access_passthrough_routes"] == ["/bedrock", "/vllm"]
assert create_data["access_vector_store_ids"] == ["vs-1", "vs-2"]
def test_update_access_group_clears_resource_fields_to_empty_list(client_and_mocks):
"""Explicit null for the new list fields is coerced to [] on update."""
client, _, mock_table, *_ = client_and_mocks
mock_table.find_unique = AsyncMock(return_value=_make_access_group_record())
resp = client.put(
"/v1/access_group/ag-123",
json={"access_passthrough_routes": None, "access_vector_store_ids": None},
)
assert resp.status_code == 200
_, update_kwargs = mock_table.update.call_args
update_data = update_kwargs["data"]
assert update_data["access_passthrough_routes"] == []
assert update_data["access_vector_store_ids"] == []
def test_create_access_group_duplicate_name_conflict(client_and_mocks):
"""Create with duplicate name returns 409."""
client, _, mock_table, *_ = client_and_mocks

View file

@ -44,6 +44,8 @@ const mockAccessGroups: AccessGroupResponse[] = [
access_model_names: [],
access_mcp_server_ids: [],
access_agent_ids: [],
access_passthrough_routes: [],
access_vector_store_ids: [],
assigned_team_ids: [],
assigned_key_ids: [],
created_at: "2025-01-01T00:00:00Z",

View file

@ -13,6 +13,8 @@ export interface AccessGroupResponse {
access_model_names: string[];
access_mcp_server_ids: string[];
access_agent_ids: string[];
access_passthrough_routes: string[];
access_vector_store_ids: string[];
assigned_team_ids: string[];
assigned_key_ids: string[];
created_at: string;

View file

@ -11,6 +11,8 @@ export interface AccessGroupCreateParams {
access_model_names?: string[];
access_mcp_server_ids?: string[];
access_agent_ids?: string[];
access_passthrough_routes?: string[];
access_vector_store_ids?: string[];
assigned_team_ids?: string[];
assigned_key_ids?: string[];
}

View file

@ -11,6 +11,8 @@ export interface AccessGroupUpdateParams {
access_model_names?: string[];
access_mcp_server_ids?: string[];
access_agent_ids?: string[];
access_passthrough_routes?: string[];
access_vector_store_ids?: string[];
assigned_team_ids?: string[];
assigned_key_ids?: string[];
}

View file

@ -51,6 +51,8 @@ const createMockAccessGroup = (overrides: Partial<AccessGroupResponse> = {}): Ac
access_model_names: ["model-1", "model-2"],
access_mcp_server_ids: ["mcp-1"],
access_agent_ids: ["agent-1"],
access_passthrough_routes: ["/bedrock"],
access_vector_store_ids: ["vs-1"],
assigned_team_ids: ["team-1"],
assigned_key_ids: ["key-1", "key-2"],
created_at: "2025-01-01T00:00:00Z",

View file

@ -15,7 +15,17 @@ import {
theme,
Typography,
} from "antd";
import { ArrowLeftIcon, BotIcon, EditIcon, KeyIcon, LayersIcon, ServerIcon, UsersIcon } from "lucide-react";
import {
ArrowLeftIcon,
BotIcon,
DatabaseIcon,
EditIcon,
KeyIcon,
LayersIcon,
RouteIcon,
ServerIcon,
UsersIcon,
} from "lucide-react";
import { useState } from "react";
import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag";
import { AccessGroupEditModal } from "./AccessGroupsModal/AccessGroupEditModal";
@ -69,6 +79,8 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr
const modelIds = accessGroup.access_model_names ?? [];
const mcpServerIds = accessGroup.access_mcp_server_ids ?? [];
const agentIds = accessGroup.access_agent_ids ?? [];
const passthroughRoutes = accessGroup.access_passthrough_routes ?? [];
const vectorStoreIds = accessGroup.access_vector_store_ids ?? [];
const keyIds = accessGroup.assigned_key_ids ?? [];
const teamIds = accessGroup.assigned_team_ids ?? [];
@ -158,6 +170,58 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr
<Empty description="No agents assigned to this group" />
),
},
{
key: "passthrough",
label: (
<Flex align="center" gap={8}>
<RouteIcon size={16} />
Pass Through Routes
<Tag>{passthroughRoutes?.length}</Tag>
</Flex>
),
children:
passthroughRoutes?.length > 0 ? (
<List
grid={{ gutter: 16, xs: 1, sm: 2, md: 3, lg: 4 }}
dataSource={passthroughRoutes}
renderItem={(route) => (
<List.Item>
<Card size="small">
<Text code>{route}</Text>
</Card>
</List.Item>
)}
/>
) : (
<Empty description="No pass through routes assigned to this group" />
),
},
{
key: "vector_stores",
label: (
<Flex align="center" gap={8}>
<DatabaseIcon size={16} />
Vector Stores
<Tag>{vectorStoreIds?.length}</Tag>
</Flex>
),
children:
vectorStoreIds?.length > 0 ? (
<List
grid={{ gutter: 16, xs: 1, sm: 2, md: 3, lg: 4 }}
dataSource={vectorStoreIds}
renderItem={(id) => (
<List.Item>
<Card size="small">
<Text code>{id}</Text>
</Card>
</List.Item>
)}
/>
) : (
<Empty description="No vector stores assigned to this group" />
),
},
];
return (

View file

@ -1,9 +1,11 @@
import { useAgents } from "@/app/(dashboard)/hooks/agents/useAgents";
import { useMCPServers } from "@/app/(dashboard)/hooks/mcpServers/useMCPServers";
import { ModelSelect } from "@/components/ModelSelect/ModelSelect";
import PassThroughRoutesSelector from "@/components/common_components/PassThroughRoutesSelector";
import VectorStoreSelector from "@/components/vector_store_management/VectorStoreSelector";
import type { FormInstance } from "antd";
import { Form, Input, Select, Space, Tabs } from "antd";
import { BotIcon, InfoIcon, LayersIcon, ServerIcon } from "lucide-react";
import { BotIcon, DatabaseIcon, InfoIcon, LayersIcon, RouteIcon, ServerIcon } from "lucide-react";
const { TextArea } = Input;
@ -13,14 +15,17 @@ export interface AccessGroupFormValues {
modelIds: string[];
mcpServerIds: string[];
agentIds: string[];
passthroughRoutes: string[];
vectorStoreIds: string[];
}
interface AccessGroupBaseFormProps {
form: FormInstance<AccessGroupFormValues>;
accessToken: string;
isNameDisabled?: boolean;
}
export function AccessGroupBaseForm({ form, isNameDisabled = false }: AccessGroupBaseFormProps) {
export function AccessGroupBaseForm({ form, accessToken, isNameDisabled = false }: AccessGroupBaseFormProps) {
const { data: agentsData } = useAgents();
const { data: mcpServersData } = useMCPServers();
@ -128,6 +133,46 @@ export function AccessGroupBaseForm({ form, isNameDisabled = false }: AccessGrou
</div>
),
},
{
key: "5",
label: (
<Space align="center" size={4}>
<RouteIcon size={16} />
Pass Through Routes
</Space>
),
children: (
<div style={{ paddingTop: 16 }}>
<Form.Item name="passthroughRoutes" label="Allowed Pass Through Routes">
<PassThroughRoutesSelector
accessToken={accessToken}
value={form.getFieldValue("passthroughRoutes") ?? []}
onChange={(values) => form.setFieldsValue({ passthroughRoutes: values })}
/>
</Form.Item>
</div>
),
},
{
key: "6",
label: (
<Space align="center" size={4}>
<DatabaseIcon size={16} />
Vector Stores
</Space>
),
children: (
<div style={{ paddingTop: 16 }}>
<Form.Item name="vectorStoreIds" label="Allowed Vector Stores">
<VectorStoreSelector
accessToken={accessToken}
value={form.getFieldValue("vectorStoreIds") ?? []}
onChange={(values) => form.setFieldsValue({ vectorStoreIds: values })}
/>
</Form.Item>
</div>
),
},
];
return (
@ -139,6 +184,8 @@ export function AccessGroupBaseForm({ form, isNameDisabled = false }: AccessGrou
modelIds: [],
mcpServerIds: [],
agentIds: [],
passthroughRoutes: [],
vectorStoreIds: [],
}}
>
<Tabs defaultActiveKey="1" items={items} />

View file

@ -6,6 +6,7 @@ import {
useCreateAccessGroup,
AccessGroupCreateParams,
} from "@/app/(dashboard)/hooks/accessGroups/useCreateAccessGroup";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
interface AccessGroupCreateModalProps {
visible: boolean;
@ -16,6 +17,7 @@ interface AccessGroupCreateModalProps {
export function AccessGroupCreateModal({ visible, onCancel, onSuccess }: AccessGroupCreateModalProps) {
const [form] = Form.useForm<AccessGroupFormValues>();
const createMutation = useCreateAccessGroup();
const { accessToken } = useAuthorized();
const handleOk = () => {
form
@ -27,6 +29,8 @@ export function AccessGroupCreateModal({ visible, onCancel, onSuccess }: AccessG
access_model_names: values.modelIds,
access_mcp_server_ids: values.mcpServerIds,
access_agent_ids: values.agentIds,
access_passthrough_routes: values.passthroughRoutes,
access_vector_store_ids: values.vectorStoreIds,
};
createMutation.mutate(params, {
@ -55,7 +59,7 @@ export function AccessGroupCreateModal({ visible, onCancel, onSuccess }: AccessG
confirmLoading={createMutation.isPending}
destroyOnClose
>
<AccessGroupBaseForm form={form} />
<AccessGroupBaseForm form={form} accessToken={accessToken ?? ""} />
</Modal>
);
}

View file

@ -4,6 +4,7 @@ import MessageManager from "@/components/molecules/message_manager";
import { AccessGroupBaseForm, AccessGroupFormValues } from "./AccessGroupBaseForm";
import { useEditAccessGroup, AccessGroupUpdateParams } from "@/app/(dashboard)/hooks/accessGroups/useEditAccessGroup";
import { AccessGroupResponse } from "@/app/(dashboard)/hooks/accessGroups/useAccessGroups";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
interface AccessGroupEditModalProps {
visible: boolean;
@ -15,6 +16,7 @@ interface AccessGroupEditModalProps {
export function AccessGroupEditModal({ visible, accessGroup, onCancel, onSuccess }: AccessGroupEditModalProps) {
const [form] = Form.useForm<AccessGroupFormValues>();
const editMutation = useEditAccessGroup();
const { accessToken } = useAuthorized();
// Populate the form with initial values whenever the modal opens or the data changes
useEffect(() => {
@ -25,6 +27,8 @@ export function AccessGroupEditModal({ visible, accessGroup, onCancel, onSuccess
modelIds: accessGroup.access_model_names ?? [],
mcpServerIds: accessGroup.access_mcp_server_ids ?? [],
agentIds: accessGroup.access_agent_ids ?? [],
passthroughRoutes: accessGroup.access_passthrough_routes ?? [],
vectorStoreIds: accessGroup.access_vector_store_ids ?? [],
});
}
}, [visible, accessGroup, form]);
@ -39,6 +43,8 @@ export function AccessGroupEditModal({ visible, accessGroup, onCancel, onSuccess
access_model_names: values.modelIds,
access_mcp_server_ids: values.mcpServerIds,
access_agent_ids: values.agentIds,
access_passthrough_routes: values.passthroughRoutes,
access_vector_store_ids: values.vectorStoreIds,
};
editMutation.mutate(
@ -69,7 +75,7 @@ export function AccessGroupEditModal({ visible, accessGroup, onCancel, onSuccess
confirmLoading={editMutation.isPending}
destroyOnHidden
>
<AccessGroupBaseForm form={form} />
<AccessGroupBaseForm form={form} accessToken={accessToken ?? ""} />
</Modal>
);
}

View file

@ -12,6 +12,8 @@ const mockAccessGroups: AccessGroupResponse[] = [
access_model_names: ["m1", "m2"],
access_mcp_server_ids: ["s1"],
access_agent_ids: ["a1"],
access_passthrough_routes: ["/bedrock"],
access_vector_store_ids: ["vs-1"],
assigned_team_ids: [],
assigned_key_ids: [],
created_at: "2024-01-15T10:00:00Z",
@ -26,6 +28,8 @@ const mockAccessGroups: AccessGroupResponse[] = [
access_model_names: ["m1"],
access_mcp_server_ids: [],
access_agent_ids: [],
access_passthrough_routes: [],
access_vector_store_ids: [],
assigned_team_ids: [],
assigned_key_ids: [],
created_at: "2024-01-10T09:00:00Z",