first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,421 @@
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { EnvVarInput } from "@/components/ui/envVarInput";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { IS_ENTERPRISE } from "@/lib/constants/config";
import { getErrorMessage, useGetCoreConfigQuery, useUpdateCoreConfigMutation } from "@/lib/store";
import { AuthConfig, CoreConfig, DefaultCoreConfig } from "@/lib/types/config";
import { EnvVar } from "@/lib/types/schemas";
import { parseArrayFromText } from "@/lib/utils/array";
import { validateOrigins } from "@/lib/utils/validation";
import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib";
import { Link } from "@tanstack/react-router";
import { AlertTriangle, Info } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
export default function SecurityView() {
const hasSettingsUpdateAccess = useRbac(RbacResource.Settings, RbacOperation.Update);
const { data: bifrostConfig } = useGetCoreConfigQuery({ fromDB: true });
const config = bifrostConfig?.client_config;
const [updateCoreConfig, { isLoading }] = useUpdateCoreConfigMutation();
const [localConfig, setLocalConfig] = useState<CoreConfig>(DefaultCoreConfig);
const hideAuthDashboard = IS_ENTERPRISE;
const [localValues, setLocalValues] = useState<{
allowed_origins: string;
allowed_headers: string;
required_headers: string;
whitelisted_routes: string;
}>({
allowed_origins: "",
allowed_headers: "",
required_headers: "",
whitelisted_routes: "",
});
const [authConfig, setAuthConfig] = useState<AuthConfig>({
admin_username: { value: "", env_var: "", from_env: false },
admin_password: { value: "", env_var: "", from_env: false },
is_enabled: false,
disable_auth_on_inference: false,
});
useEffect(() => {
if (bifrostConfig && config) {
setLocalConfig(config);
setLocalValues({
allowed_origins: config?.allowed_origins?.join(", ") || "",
allowed_headers: config?.allowed_headers?.join(", ") || "",
required_headers: config?.required_headers?.join(", ") || "",
whitelisted_routes: config?.whitelisted_routes?.join(", ") || "",
});
}
if (bifrostConfig?.auth_config) {
setAuthConfig(bifrostConfig.auth_config);
}
}, [config, bifrostConfig]);
const hasChanges = useMemo(() => {
if (!config) return false;
const localOrigins = localConfig.allowed_origins?.slice().sort().join(",");
const serverOrigins = config.allowed_origins?.slice().sort().join(",");
const originsChanged = localOrigins !== serverOrigins;
const localHeaders = localConfig.allowed_headers?.slice().sort().join(",");
const serverHeaders = config.allowed_headers?.slice().sort().join(",");
const headersChanged = localHeaders !== serverHeaders;
const usernameChanged =
authConfig.admin_username?.value !== bifrostConfig?.auth_config?.admin_username?.value ||
authConfig.admin_username?.env_var !== bifrostConfig?.auth_config?.admin_username?.env_var ||
authConfig.admin_username?.from_env !== bifrostConfig?.auth_config?.admin_username?.from_env;
const passwordChanged =
authConfig.admin_password?.value !== bifrostConfig?.auth_config?.admin_password?.value ||
authConfig.admin_password?.env_var !== bifrostConfig?.auth_config?.admin_password?.env_var ||
authConfig.admin_password?.from_env !== bifrostConfig?.auth_config?.admin_password?.from_env;
const authChanged =
authConfig.is_enabled !== bifrostConfig?.auth_config?.is_enabled ||
usernameChanged ||
passwordChanged ||
authConfig.disable_auth_on_inference !== bifrostConfig?.auth_config?.disable_auth_on_inference;
const localRequired = localConfig.required_headers?.slice().sort().join(",");
const serverRequired = config.required_headers?.slice().sort().join(",");
const requiredChanged = localRequired !== serverRequired;
const localWhitelistedRoutes = localConfig.whitelisted_routes?.slice().sort().join(",");
const serverWhitelistedRoutes = config.whitelisted_routes?.slice().sort().join(",");
const whitelistedRoutesChanged = localWhitelistedRoutes !== serverWhitelistedRoutes;
const enforceAuthOnInferenceChanged = localConfig.enforce_auth_on_inference !== config.enforce_auth_on_inference;
const allowDirectKeysChanged = localConfig.allow_direct_keys !== config.allow_direct_keys;
return (
originsChanged ||
headersChanged ||
requiredChanged ||
whitelistedRoutesChanged ||
authChanged ||
enforceAuthOnInferenceChanged ||
allowDirectKeysChanged
);
}, [config, localConfig, authConfig, bifrostConfig]);
const needsRestart = useMemo(() => {
if (!config) return false;
const localOrigins = localConfig.allowed_origins?.slice().sort().join(",");
const serverOrigins = config.allowed_origins?.slice().sort().join(",");
const originsChanged = localOrigins !== serverOrigins;
const localHeaders = localConfig.allowed_headers?.slice().sort().join(",");
const serverHeaders = config.allowed_headers?.slice().sort().join(",");
const headersChanged = localHeaders !== serverHeaders;
const enforceAuthOnInferenceChanged = localConfig.enforce_auth_on_inference !== config.enforce_auth_on_inference && IS_ENTERPRISE;
return originsChanged || headersChanged || enforceAuthOnInferenceChanged;
}, [config, localConfig]);
const handleAllowedOriginsChange = useCallback((value: string) => {
setLocalValues((prev) => ({ ...prev, allowed_origins: value }));
setLocalConfig((prev) => ({ ...prev, allowed_origins: parseArrayFromText(value) }));
}, []);
const handleAllowedHeadersChange = useCallback((value: string) => {
setLocalValues((prev) => ({ ...prev, allowed_headers: value }));
setLocalConfig((prev) => ({ ...prev, allowed_headers: parseArrayFromText(value) }));
}, []);
const handleRequiredHeadersChange = useCallback((value: string) => {
setLocalValues((prev) => ({ ...prev, required_headers: value }));
setLocalConfig((prev) => ({ ...prev, required_headers: parseArrayFromText(value) }));
}, []);
const handleWhitelistedRoutesChange = useCallback((value: string) => {
setLocalValues((prev) => ({ ...prev, whitelisted_routes: value }));
setLocalConfig((prev) => ({ ...prev, whitelisted_routes: parseArrayFromText(value) }));
}, []);
const handleConfigChange = useCallback((field: keyof CoreConfig, value: boolean) => {
setLocalConfig((prev) => ({ ...prev, [field]: value }));
}, []);
const handleAuthToggle = useCallback((checked: boolean) => {
setAuthConfig((prev) => ({ ...prev, is_enabled: checked }));
}, []);
const handleDisableAuthOnInferenceToggle = useCallback((checked: boolean) => {
setAuthConfig((prev) => ({ ...prev, disable_auth_on_inference: checked }));
}, []);
const handleAuthFieldChange = useCallback((field: "admin_username" | "admin_password", value: EnvVar) => {
setAuthConfig((prev) => ({ ...prev, [field]: value }));
}, []);
const handleSave = useCallback(async () => {
try {
const validation = validateOrigins(localConfig.allowed_origins);
if (!validation.isValid && localConfig.allowed_origins.length > 0) {
toast.error(
`Invalid origins: ${validation.invalidOrigins.join(", ")}. Origins must be valid URLs like https://example.com, wildcard patterns like https://*.example.com, or "*" to allow all origins`,
);
return;
}
const hasUsername = authConfig.admin_username?.value || authConfig.admin_username?.env_var;
const hasPassword = authConfig.admin_password?.value || authConfig.admin_password?.env_var;
await updateCoreConfig({
...bifrostConfig!,
client_config: localConfig,
auth_config: authConfig.is_enabled && hasUsername && hasPassword ? authConfig : { ...authConfig, is_enabled: false },
}).unwrap();
toast.success("Security settings updated successfully.");
} catch (error) {
toast.error(getErrorMessage(error));
}
}, [bifrostConfig, localConfig, authConfig, updateCoreConfig]);
return (
<div className="mx-auto w-full max-w-4xl space-y-4">
<div>
<h2 className="text-lg font-semibold tracking-tight">Security Settings</h2>
<p className="text-muted-foreground text-sm">Configure security and access control settings.</p>
</div>
<div className="space-y-4">
{authConfig.is_enabled && !authConfig.disable_auth_on_inference && (
<Alert variant="default" className="border-blue-20">
<Info className="h-4 w-4 text-blue-600" />
<AlertDescription>
You will need to use Basic Auth for all your inference calls (including MCP tool execution). You can disable it below. Check{" "}
<Link to="/workspace/config/api-keys" className="text-md text-primary underline">
API Keys
</Link>
</AlertDescription>
</Alert>
)}
{authConfig.is_enabled && authConfig.disable_auth_on_inference && (
<Alert variant="default" className="border-blue-20">
<Info className="h-4 w-4 text-blue-600" />
<AlertDescription>
Authentication is disabled for inference calls. Only dashboard, admin API and MCP tool execution calls require authentication.
</AlertDescription>
</Alert>
)}
{/* Password Protect the Dashboard */}
{!hideAuthDashboard && (
<div>
<div className="space-y-4 rounded-lg border p-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="auth-enabled" className="text-sm font-medium">
Password protect the dashboard <Badge variant="secondary">BETA</Badge>
</Label>
<p className="text-muted-foreground text-sm">
Set up authentication credentials to protect your Bifrost dashboard. Once configured, use the generated token for all
admin API calls.
</p>
</div>
<Switch id="auth-enabled" checked={authConfig.is_enabled} onCheckedChange={handleAuthToggle} />
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="admin-username">Username</Label>
<EnvVarInput
id="admin-username"
type="text"
placeholder="Enter admin username or env.VAR_NAME"
value={authConfig.admin_username}
disabled={!authConfig.is_enabled}
onChange={(value) => handleAuthFieldChange("admin_username", value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="admin-password">Password</Label>
<EnvVarInput
id="admin-password"
type="password"
placeholder="Enter admin password or env.VAR_NAME"
value={authConfig.admin_password}
disabled={!authConfig.is_enabled}
onChange={(value) => handleAuthFieldChange("admin_password", value)}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="disable-auth-inference" className="text-sm font-medium">
Disable authentication on inference calls
</Label>
<p className="text-muted-foreground text-sm">
When enabled, inference API calls (chat completions, embeddings, etc.) will not require authentication. Dashboard and
admin API calls will still require authentication.
</p>
</div>
<Switch
id="disable-auth-inference"
className="ml-5"
checked={authConfig.disable_auth_on_inference ?? false}
disabled={!authConfig.is_enabled}
onCheckedChange={handleDisableAuthOnInferenceToggle}
/>
</div>
</div>
</div>
</div>
)}
{/* Enable Auth on Inference */}
<div className="flex items-center justify-between space-x-2 rounded-lg border p-4">
<div className="space-y-0.5">
<label htmlFor="enforce-auth-on-inference" className="text-sm font-medium">
{IS_ENTERPRISE ? "Enable Auth on Inference" : "Enforce Virtual Keys on Inference"}
</label>
<p className="text-muted-foreground text-sm">
{IS_ENTERPRISE
? "Require authentication (virtual key, API key, or user token) for all inference endpoints."
: "Require a virtual key for all inference requests."}{" "}
See{" "}
<a
href="https://docs.getbifrost.ai/features/governance/virtual-keys"
target="_blank"
rel="noopener noreferrer"
className="text-primary underline"
data-testid="security-virtual-keys-docs-link"
>
documentation
</a>{" "}
for details.
</p>
</div>
<Switch
id="enforce-auth-on-inference"
data-testid="enforce-auth-on-inference-switch"
checked={localConfig.enforce_auth_on_inference}
onCheckedChange={(checked) => handleConfigChange("enforce_auth_on_inference", checked)}
/>
</div>
{/* Allowed Origins */}
{needsRestart && <RestartWarning />}
{/* Allow Direct API Keys */}
<div className="flex items-center justify-between space-x-2 rounded-lg border p-4">
<div className="space-y-0.5">
<label htmlFor="allow-direct-keys" className="text-sm font-medium">
Allow Direct API Keys
</label>
<p className="text-muted-foreground text-sm">
Allow API keys to be passed directly in request headers (<b>Authorization</b>, <b>x-api-key</b>, or <b>x-goog-api-key</b>).
Bifrost will directly use the key.
</p>
</div>
<Switch
id="allow-direct-keys"
checked={localConfig.allow_direct_keys}
onCheckedChange={(checked) => handleConfigChange("allow_direct_keys", checked)}
/>
</div>
<div>
<div className="space-y-2 rounded-lg border p-4">
<div className="space-y-0.5">
<label htmlFor="allowed-origins" className="text-sm font-medium">
Allowed Origins
</label>
<p className="text-muted-foreground text-sm">
Comma-separated list of allowed origins for CORS and WebSocket connections. Localhost origins are always allowed. Each
origin must be a complete URL with protocol (e.g., https://app.example.com, http://10.0.0.100:3000). Wildcards are supported
for subdomains (e.g., https://*.example.com) or use "*" to allow all origins.
</p>
</div>
<Textarea
id="allowed-origins"
className="h-24"
placeholder="https://app.example.com, https://*.example.com, *"
value={localValues.allowed_origins}
onChange={(e) => handleAllowedOriginsChange(e.target.value)}
/>
</div>
</div>
{/* Allowed Headers */}
<div>
<div className="space-y-2 rounded-lg border p-4">
<div className="space-y-0.5">
<label htmlFor="allowed-headers" className="text-sm font-medium">
Allowed Headers
</label>
<p className="text-muted-foreground text-sm">Comma-separated list of allowed headers for CORS.</p>
</div>
<Textarea
id="allowed-headers"
className="h-24"
placeholder="X-Stainless-Timeout"
value={localValues.allowed_headers}
onChange={(e) => handleAllowedHeadersChange(e.target.value)}
/>
</div>
</div>
{/* Required Headers */}
<div>
<div className="space-y-2 rounded-lg border p-4">
<div className="space-y-0.5">
<label htmlFor="required-headers" className="text-sm font-medium">
Required Headers
</label>
<p className="text-muted-foreground text-sm">
Comma-separated list of headers that must be present on every request. Requests missing any of these headers will be
rejected with a 400 error. Header names are case-insensitive.
</p>
</div>
<Textarea
id="required-headers"
data-testid="required-headers-textarea"
className="h-24"
placeholder="X-Tenant-ID, X-Custom-Header"
value={localValues.required_headers}
onChange={(e) => handleRequiredHeadersChange(e.target.value)}
/>
</div>
</div>
{/* Whitelisted Routes */}
<div>
<div className="space-y-2 rounded-lg border p-4">
<div className="space-y-0.5">
<label htmlFor="whitelisted-routes" className="text-sm font-medium">
Whitelisted Routes
</label>
<p className="text-muted-foreground text-sm">
Comma-separated list of routes that bypass the auth middleware. Requests to these routes will not require authentication.
System routes like <b>/health</b>, <b>/api/session/login</b>, and <b>/api/session/is-auth-enabled</b> are always whitelisted
regardless of this setting.
</p>
</div>
<Textarea
id="whitelisted-routes"
data-testid="whitelisted-routes-textarea"
className="h-24"
placeholder="/api/custom-webhook, /api/public-endpoint"
value={localValues.whitelisted_routes}
onChange={(e) => handleWhitelistedRoutesChange(e.target.value)}
/>
</div>
</div>
</div>
<div className="flex justify-end pt-2">
<Button onClick={handleSave} disabled={!hasChanges || isLoading || !hasSettingsUpdateAccess}>
{isLoading ? "Saving..." : "Save Changes"}
</Button>
</div>
</div>
);
}
const RestartWarning = () => {
return (
<Alert variant="destructive" className="mt-2">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>Need to restart Bifrost to apply changes.</AlertDescription>
</Alert>
);
};