diff --git a/framework/configstore/rdb.go b/framework/configstore/rdb.go index dc95f5f39b..f104ed0328 100644 --- a/framework/configstore/rdb.go +++ b/framework/configstore/rdb.go @@ -2417,8 +2417,6 @@ func (s *RDBConfigStore) GetVirtualKeysPaginated(ctx context.Context, params Vir baseQuery = baseQuery.Where("customer_id = ?", params.CustomerID) } else if params.TeamID != "" { baseQuery = baseQuery.Where("team_id = ?", params.TeamID) - } else if params.AccessProfileID > 0 { - baseQuery = baseQuery.Where("access_profile_id = ?", params.AccessProfileID) } if params.Search != "" { search := "%" + strings.ToLower(params.Search) + "%" diff --git a/framework/configstore/store.go b/framework/configstore/store.go index 451ba86c58..196a6ae3cd 100644 --- a/framework/configstore/store.go +++ b/framework/configstore/store.go @@ -20,7 +20,6 @@ type VirtualKeyQueryParams struct { Search string CustomerID string TeamID string - AccessProfileID uint // When set, return VKs attached to this access profile template SortBy string // name, budget_spent, created_at, status (default: created_at) Order string // asc, desc (default: asc) Export bool // When true, skip default pagination limits (caller controls limit) diff --git a/framework/configstore/tables/virtualkey.go b/framework/configstore/tables/virtualkey.go index bf610e849e..d2f682238d 100644 --- a/framework/configstore/tables/virtualkey.go +++ b/framework/configstore/tables/virtualkey.go @@ -215,11 +215,10 @@ type TableVirtualKey struct { ProviderConfigs []TableVirtualKeyProviderConfig `gorm:"foreignKey:VirtualKeyID;constraint:OnDelete:CASCADE" json:"provider_configs"` // Empty means no providers allowed (deny-by-default) MCPConfigs []TableVirtualKeyMCPConfig `gorm:"foreignKey:VirtualKeyID;constraint:OnDelete:CASCADE" json:"mcp_configs"` - // Foreign key relationships (mutually exclusive: TeamID, CustomerID, or AccessProfileID) - TeamID *string `gorm:"type:varchar(255);index" json:"team_id,omitempty"` - CustomerID *string `gorm:"type:varchar(255);index" json:"customer_id,omitempty"` - AccessProfileID *uint `gorm:"index" json:"access_profile_id,omitempty"` - RateLimitID *string `gorm:"type:varchar(255);index" json:"rate_limit_id,omitempty"` + // Foreign key relationships (mutually exclusive: either TeamID or CustomerID, not both) + TeamID *string `gorm:"type:varchar(255);index" json:"team_id,omitempty"` + CustomerID *string `gorm:"type:varchar(255);index" json:"customer_id,omitempty"` + RateLimitID *string `gorm:"type:varchar(255);index" json:"rate_limit_id,omitempty"` CalendarAligned bool `gorm:"default:false" json:"calendar_aligned"` @@ -254,22 +253,13 @@ func (vk *TableVirtualKey) IsActiveValue() bool { return *vk.IsActive } -// BeforeSave is a GORM hook that enforces mutual exclusion among team, customer, and -// access profile attachments, computes a SHA-256 hash of the plaintext value for indexed -// lookups, and encrypts the virtual key value before writing to the database. +// BeforeSave is a GORM hook that enforces mutual exclusion (team vs customer), computes +// a SHA-256 hash of the plaintext value for indexed lookups, and encrypts the virtual key +// value before writing to the database. func (vk *TableVirtualKey) BeforeSave(tx *gorm.DB) error { - entityCount := 0 - if vk.TeamID != nil { - entityCount++ - } - if vk.CustomerID != nil { - entityCount++ - } - if vk.AccessProfileID != nil && *vk.AccessProfileID > 0 { - entityCount++ - } - if entityCount > 1 { - return fmt.Errorf("virtual key can only be attached to one of: team, customer, or access profile") + // Enforce mutual exclusion: VK can belong to either Team OR Customer, not both + if vk.TeamID != nil && vk.CustomerID != nil { + return fmt.Errorf("virtual key cannot belong to both team and customer") } // Hash must be computed before encryption (from plaintext value) diff --git a/transports/bifrost-http/handlers/governance.go b/transports/bifrost-http/handlers/governance.go index ee4ff77b09..6e542e667a 100644 --- a/transports/bifrost-http/handlers/governance.go +++ b/transports/bifrost-http/handlers/governance.go @@ -92,10 +92,9 @@ type CreateVirtualKeyRequest struct { MCPClientName string `json:"mcp_client_name" validate:"required"` ToolsToExecute schemas.WhiteList `json:"tools_to_execute,omitempty"` } `json:"mcp_configs,omitempty"` // Empty means no MCP clients allowed (deny-by-default) - TeamID *string `json:"team_id,omitempty"` // Mutually exclusive with CustomerID and AccessProfileID - CustomerID *string `json:"customer_id,omitempty"` // Mutually exclusive with TeamID and AccessProfileID - AccessProfileID *uint `json:"access_profile_id,omitempty"` // Mutually exclusive with TeamID and CustomerID - Budgets []CreateBudgetRequest `json:"budgets,omitempty"` // Multi-budget: each must have a unique reset_duration + TeamID *string `json:"team_id,omitempty"` // Mutually exclusive with CustomerID + CustomerID *string `json:"customer_id,omitempty"` // Mutually exclusive with TeamID + Budgets []CreateBudgetRequest `json:"budgets,omitempty"` // Multi-budget: each must have a unique reset_duration RateLimit *CreateRateLimitRequest `json:"rate_limit,omitempty"` IsActive *bool `json:"is_active,omitempty"` CalendarAligned bool `json:"calendar_aligned,omitempty"` // When true, all budgets reset at clean calendar boundaries @@ -122,7 +121,6 @@ type UpdateVirtualKeyRequest struct { } `json:"mcp_configs,omitempty"` TeamID *string `json:"team_id,omitempty"` CustomerID *string `json:"customer_id,omitempty"` - AccessProfileID *uint `json:"access_profile_id,omitempty"` Budgets []CreateBudgetRequest `json:"budgets,omitempty"` // Multi-budget: replaces all VK-level budgets RateLimit *UpdateRateLimitRequest `json:"rate_limit,omitempty"` IsActive *bool `json:"is_active,omitempty"` @@ -477,13 +475,12 @@ func (h *GovernanceHandler) getVirtualKeys(ctx *fasthttp.RequestCtx) { search := string(ctx.QueryArgs().Peek("search")) customerID := string(ctx.QueryArgs().Peek("customer_id")) teamID := string(ctx.QueryArgs().Peek("team_id")) - accessProfileIDStr := string(ctx.QueryArgs().Peek("access_profile_id")) sortBy := string(ctx.QueryArgs().Peek("sort_by")) order := string(ctx.QueryArgs().Peek("order")) isExport := string(ctx.QueryArgs().Peek("export")) == "true" excludeAccessProfileManagedVirtual := string(ctx.QueryArgs().Peek("exclude_access_profile_managed_virtual")) == "true" - if limitStr != "" || offsetStr != "" || search != "" || customerID != "" || teamID != "" || accessProfileIDStr != "" || sortBy != "" || isExport || excludeAccessProfileManagedVirtual { + if limitStr != "" || offsetStr != "" || search != "" || customerID != "" || teamID != "" || sortBy != "" || isExport || excludeAccessProfileManagedVirtual { // Paginated/filtered path params := configstore.VirtualKeyQueryParams{ Search: search, @@ -494,18 +491,6 @@ func (h *GovernanceHandler) getVirtualKeys(ctx *fasthttp.RequestCtx) { Export: isExport, ExcludeAccessProfileManagedVirtual: excludeAccessProfileManagedVirtual, } - if accessProfileIDStr != "" { - apID, err := strconv.ParseUint(accessProfileIDStr, 10, 0) - if err != nil { - SendError(ctx, 400, "Invalid access_profile_id parameter: must be a number") - return - } - if (customerID != "" || teamID != "") && apID > 0 { - SendError(ctx, 400, "access_profile_id cannot be combined with team_id or customer_id filters") - return - } - params.AccessProfileID = uint(apID) - } if limitStr != "" { n, err := strconv.Atoi(limitStr) if err != nil { @@ -580,19 +565,9 @@ func (h *GovernanceHandler) createVirtualKey(ctx *fasthttp.RequestCtx) { SendError(ctx, 400, "Virtual key name is required") return } - // Validate mutually exclusive TeamID, CustomerID, and AccessProfileID - entityCount := 0 - if req.TeamID != nil { - entityCount++ - } - if req.CustomerID != nil { - entityCount++ - } - if req.AccessProfileID != nil && *req.AccessProfileID > 0 { - entityCount++ - } - if entityCount > 1 { - SendError(ctx, 400, "VirtualKey can only be attached to one of: Team, Customer, or AccessProfile") + // Validate mutually exclusive TeamID and CustomerID + if req.TeamID != nil && req.CustomerID != nil { + SendError(ctx, 400, "VirtualKey cannot be attached to both Team and Customer") return } // Validate budgets if provided @@ -638,7 +613,6 @@ func (h *GovernanceHandler) createVirtualKey(ctx *fasthttp.RequestCtx) { Description: req.Description, TeamID: req.TeamID, CustomerID: req.CustomerID, - AccessProfileID: req.AccessProfileID, IsActive: isActive, CalendarAligned: req.CalendarAligned, } @@ -870,19 +844,9 @@ func (h *GovernanceHandler) updateVirtualKey(ctx *fasthttp.RequestCtx) { SendError(ctx, 400, "Invalid JSON") return } - // Validate mutually exclusive TeamID, CustomerID, and AccessProfileID - entityCount := 0 - if req.TeamID != nil { - entityCount++ - } - if req.CustomerID != nil { - entityCount++ - } - if req.AccessProfileID != nil && *req.AccessProfileID > 0 { - entityCount++ - } - if entityCount > 1 { - SendError(ctx, 400, "VirtualKey can only be attached to one of: Team, Customer, or AccessProfile") + // Validate mutually exclusive TeamID and CustomerID + if req.TeamID != nil && req.CustomerID != nil { + SendError(ctx, 400, "VirtualKey cannot be attached to both Team and Customer") return } vk, err := h.configStore.GetVirtualKey(ctx, vkID) @@ -934,21 +898,16 @@ func (h *GovernanceHandler) updateVirtualKey(ctx *fasthttp.RequestCtx) { } if req.TeamID != nil { vk.TeamID = req.TeamID - vk.CustomerID = nil - vk.AccessProfileID = nil - } else if req.CustomerID != nil { + vk.CustomerID = nil // Clear CustomerID if setting TeamID + } + if req.CustomerID != nil { vk.CustomerID = req.CustomerID - vk.TeamID = nil - vk.AccessProfileID = nil - } else if req.AccessProfileID != nil { - vk.AccessProfileID = req.AccessProfileID - vk.TeamID = nil - vk.CustomerID = nil - } else { - // All nil — clear entity assignment + vk.TeamID = nil // Clear TeamID if setting CustomerID + } + // When both TeamID and CustomerID are nil + if req.TeamID == nil && req.CustomerID == nil { vk.TeamID = nil vk.CustomerID = nil - vk.AccessProfileID = nil } if req.IsActive != nil { vk.IsActive = req.IsActive @@ -1084,13 +1043,6 @@ func (h *GovernanceHandler) updateVirtualKey(ctx *fasthttp.RequestCtx) { if err := h.configStore.UpdateVirtualKey(ctx, vk, tx); err != nil { return err } - // UpdateVirtualKey's Select list excludes access_profile_id to protect config-sync paths. - // Persist it separately so API-driven entity assignment is always saved. - if err := tx.Model(&configstoreTables.TableVirtualKey{}). - Where("id = ?", vk.ID). - Updates(map[string]interface{}{"access_profile_id": vk.AccessProfileID}).Error; err != nil { - return fmt.Errorf("failed to update virtual key access_profile_id: %w", err) - } if req.ProviderConfigs != nil { // Get existing provider configs for comparison var existingConfigs []configstoreTables.TableVirtualKeyProviderConfig diff --git a/ui/app/_fallbacks/enterprise/lib/store/apis/accessProfileApi.ts b/ui/app/_fallbacks/enterprise/lib/store/apis/accessProfileApi.ts index 7d162e5a33..c5038baeee 100644 --- a/ui/app/_fallbacks/enterprise/lib/store/apis/accessProfileApi.ts +++ b/ui/app/_fallbacks/enterprise/lib/store/apis/accessProfileApi.ts @@ -1,22 +1,7 @@ -import { GetAccessProfilesParams, GetAccessProfilesResponse, GetUserAccessProfilesResponse } from "@enterprise/lib/types/accessProfile"; +import { GetUserAccessProfilesResponse } from "@enterprise/lib/types/accessProfile"; // OSS build has no access-profile backend — return undefined data so consumers -// fall back gracefully (empty lists, no AP-specific UI rendered). -export const useGetAccessProfilesQuery = ( - _params?: GetAccessProfilesParams | void, - _opts?: { skip?: boolean }, -): { - data: GetAccessProfilesResponse | undefined; - isLoading: boolean; - isError: boolean; - error: null; -} => ({ - data: undefined, - isLoading: false, - isError: false, - error: null, -}); - +// (e.g. useVirtualKeyUsage) fall back to VK-owned budget/rate-limit values. export const useGetUserAccessProfilesQuery = ( _userId: string, _opts?: { skip?: boolean; pollingInterval?: number }, diff --git a/ui/app/_fallbacks/enterprise/lib/types/accessProfile.ts b/ui/app/_fallbacks/enterprise/lib/types/accessProfile.ts index 22d245a4fd..414743dafb 100644 --- a/ui/app/_fallbacks/enterprise/lib/types/accessProfile.ts +++ b/ui/app/_fallbacks/enterprise/lib/types/accessProfile.ts @@ -38,20 +38,4 @@ export interface UserAccessProfile { export interface GetUserAccessProfilesResponse { access_profiles: UserAccessProfile[]; -} - -export interface GetAccessProfilesParams { - limit?: number; - offset?: number; - search?: string; - tags?: string; - is_active?: boolean; -} - -export interface GetAccessProfilesResponse { - access_profiles: { id: number; name: string; [key: string]: unknown }[]; - count: number; - total_count: number; - limit: number; - offset: number; } \ No newline at end of file diff --git a/ui/app/workspace/virtual-keys/views/virtualKeyDetailsSheet.tsx b/ui/app/workspace/virtual-keys/views/virtualKeyDetailsSheet.tsx index a413a8df16..953cf06c04 100644 --- a/ui/app/workspace/virtual-keys/views/virtualKeyDetailsSheet.tsx +++ b/ui/app/workspace/virtual-keys/views/virtualKeyDetailsSheet.tsx @@ -50,8 +50,7 @@ interface VirtualKeyDetailSheetProps { } export default function VirtualKeyDetailSheet({ virtualKey, onClose }: VirtualKeyDetailSheetProps) { - const { assignedUsers, managingProfile, hasApRateLimit, displayBudgets, displayRateLimit } = useVirtualKeyUsage(virtualKey); - const isManagedByProfile = !!virtualKey.access_profile_id; + const { assignedUsers, managingProfile, isManagedByProfile, hasApRateLimit, displayBudgets, displayRateLimit } = useVirtualKeyUsage(virtualKey); const getEntityInfo = () => { if (virtualKey.team) { diff --git a/ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx b/ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx index 7de9e77a4a..2788d7984e 100644 --- a/ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx +++ b/ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx @@ -33,7 +33,6 @@ import { Customer, Team, VirtualKey } from "@/lib/types/governance"; import { cn } from "@/lib/utils"; import { formatCurrency } from "@/lib/utils/governance"; import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib"; -import { useGetAccessProfilesQuery } from "@enterprise/lib/store/apis/accessProfileApi"; import { ArrowDown, ArrowUp, @@ -80,8 +79,6 @@ function virtualKeysToCSV(vks: VirtualKey[], accessProfileNames: Record formatCurrency(b.max_limit)).join("; ") : ""; const budgetSpent = vk.budgets?.length ? vk.budgets.map((b) => formatCurrency(b.current_usage)).join("; ") : ""; @@ -295,15 +292,6 @@ export default function VirtualKeysTable({ const [showBulkRotateDialog, setShowBulkRotateDialog] = useState(false); const [fetchVirtualKeys, { isFetching: isExporting }] = useLazyGetVirtualKeysQuery(); - const { data: accessProfilesData } = useGetAccessProfilesQuery({ limit: 100 }); - const accessProfileNames = useMemo(() => { - const map: Record = {}; - for (const ap of accessProfilesData?.access_profiles ?? []) { - map[ap.id] = ap.name; - } - return map; - }, [accessProfilesData]); - // Derive objects from props so they stay in sync with RTK cache updates const editingVirtualKey = useMemo( () => (editingVirtualKeyId ? (virtualKeys.find((vk) => vk.id === editingVirtualKeyId) ?? null) : null), @@ -477,7 +465,7 @@ export default function VirtualKeysTable({ const handleExportCSV = async () => { if (exportScope === "current_page") { - downloadCSV(virtualKeysToCSV(virtualKeys, accessProfileNames)); + downloadCSV(virtualKeysToCSV(virtualKeys)); toast.success(`Exported ${virtualKeys.length} virtual keys`); setShowExportDialog(false); return; @@ -499,7 +487,7 @@ export default function VirtualKeysTable({ export: true, }).unwrap(); - downloadCSV(virtualKeysToCSV(result.virtual_keys, accessProfileNames)); + downloadCSV(virtualKeysToCSV(result.virtual_keys)); toast.success(`Exported ${result.virtual_keys.length} virtual keys`); setShowExportDialog(false); } catch (error) { @@ -797,10 +785,6 @@ export default function VirtualKeysTable({ Customer: {vk.customer.name} - ) : vk.access_profile_id ? ( - - AP: {accessProfileNames[vk.access_profile_id] ?? vk.access_profile_id} - ) : ( - )} diff --git a/ui/components/ui/combobox.tsx b/ui/components/ui/combobox.tsx index 35dfd6a17e..3cda97d167 100644 --- a/ui/components/ui/combobox.tsx +++ b/ui/components/ui/combobox.tsx @@ -295,7 +295,6 @@ interface ComboboxSelectBaseProps { hideClear?: boolean; className?: string; emptyMessage?: string; - "data-testid"?: string; } interface ComboboxSelectSingleProps extends ComboboxSelectBaseProps { @@ -321,7 +320,6 @@ function ComboboxSelect(props: ComboboxSelectProps) { className, emptyMessage = "No results found.", noPortal, - "data-testid": dataTestId, } = props; const [open, setOpen] = React.useState(false); @@ -444,7 +442,6 @@ function ComboboxSelect(props: ComboboxSelectProps) { role="combobox" aria-expanded={open} disabled={disabled} - data-testid={dataTestId} className={cn( "h-8 w-full justify-between !bg-transparent font-normal active:scale-none", !selectedLabel && "text-muted-foreground", diff --git a/ui/lib/store/apis/baseApi.ts b/ui/lib/store/apis/baseApi.ts index c040b517ab..671ffd3155 100644 --- a/ui/lib/store/apis/baseApi.ts +++ b/ui/lib/store/apis/baseApi.ts @@ -184,7 +184,6 @@ export const baseApi = createApi({ "Versions", "Sessions", "AccessProfiles", - "AccessProfileVirtualKeys", "BusinessUnits", "PromptDeployments", "AuthType", diff --git a/ui/lib/store/apis/governanceApi.ts b/ui/lib/store/apis/governanceApi.ts index 9770b010d1..0739b28df4 100644 --- a/ui/lib/store/apis/governanceApi.ts +++ b/ui/lib/store/apis/governanceApi.ts @@ -63,7 +63,6 @@ export const governanceApi = baseApi.injectEndpoints({ ...(params?.search && { search: params.search }), ...(params?.customer_id && { customer_id: params.customer_id }), ...(params?.team_id && { team_id: params.team_id }), - ...(params?.access_profile_id !== undefined && { access_profile_id: params.access_profile_id }), ...(params?.exclude_access_profile_managed_virtual === true && { exclude_access_profile_managed_virtual: "true", }), diff --git a/ui/lib/types/governance.ts b/ui/lib/types/governance.ts index 8c6a9dc7e6..c6e3c39160 100644 --- a/ui/lib/types/governance.ts +++ b/ui/lib/types/governance.ts @@ -72,7 +72,6 @@ export interface VirtualKey { mcp_configs?: VirtualKeyMCPConfig[]; team_id?: string; customer_id?: string; - access_profile_id?: number; rate_limit_id?: string; is_active: boolean; calendar_aligned?: boolean; @@ -161,7 +160,6 @@ export interface CreateVirtualKeyRequest { mcp_configs?: VirtualKeyMCPConfigRequest[]; team_id?: string; customer_id?: string; - access_profile_id?: number; budgets?: CreateBudgetRequest[]; rate_limit?: CreateRateLimitRequest; is_active?: boolean; @@ -175,7 +173,6 @@ export interface UpdateVirtualKeyRequest { mcp_configs?: VirtualKeyMCPConfigRequest[]; team_id?: string; customer_id?: string; - access_profile_id?: number; budgets?: CreateBudgetRequest[]; rate_limit?: UpdateRateLimitRequest; is_active?: boolean; @@ -259,7 +256,6 @@ export interface GetVirtualKeysParams { search?: string; customer_id?: string; team_id?: string; - access_profile_id?: number; exclude_access_profile_managed_virtual?: boolean; sort_by?: "name" | "budget_spent" | "created_at" | "status"; order?: "asc" | "desc";