mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-19 20:24:32 +02:00
Add basic passthrough auth (#3731)
* Add basic passthrough auth * Add server-side validation * Disallow for non-oauth * Fix npm build
This commit is contained in:
@@ -24,6 +24,14 @@ import debounce from "lodash/debounce";
|
||||
import { AdvancedOptionsToggle } from "@/components/AdvancedOptionsToggle";
|
||||
import Link from "next/link";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useAuthType } from "@/lib/hooks";
|
||||
|
||||
function parseJsonWithTrailingCommas(jsonString: string) {
|
||||
// Regular expression to remove trailing commas before } or ]
|
||||
@@ -51,7 +59,11 @@ function ToolForm({
|
||||
}: {
|
||||
existingTool?: ToolSnapshot;
|
||||
values: ToolFormValues;
|
||||
setFieldValue: (field: string, value: string) => void;
|
||||
setFieldValue: <T = any>(
|
||||
field: string,
|
||||
value: T,
|
||||
shouldValidate?: boolean
|
||||
) => void;
|
||||
isSubmitting: boolean;
|
||||
definitionErrorState: [
|
||||
string | null,
|
||||
@@ -65,6 +77,9 @@ function ToolForm({
|
||||
const [definitionError, setDefinitionError] = definitionErrorState;
|
||||
const [methodSpecs, setMethodSpecs] = methodSpecsState;
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
const authType = useAuthType();
|
||||
const isOAuthEnabled = authType === "oidc" || authType === "google_oauth";
|
||||
|
||||
const debouncedValidateDefinition = useCallback(
|
||||
(definition: string) => {
|
||||
const validateDefinition = async () => {
|
||||
@@ -218,43 +233,38 @@ function ToolForm({
|
||||
</p>
|
||||
<FieldArray
|
||||
name="customHeaders"
|
||||
render={(arrayHelpers: ArrayHelpers) => (
|
||||
<div className="space-y-4">
|
||||
{values.customHeaders && values.customHeaders.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{values.customHeaders.map(
|
||||
(
|
||||
header: { key: string; value: string },
|
||||
index: number
|
||||
) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center space-x-2 bg-gray-50 p-3 rounded-lg shadow-sm"
|
||||
render={(arrayHelpers) => (
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
{values.customHeaders.map(
|
||||
(header: { key: string; value: string }, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center space-x-2 bg-gray-50 p-3 rounded-lg shadow-sm"
|
||||
>
|
||||
<Field
|
||||
name={`customHeaders.${index}.key`}
|
||||
placeholder="Header Key"
|
||||
className="flex-1 p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
/>
|
||||
<Field
|
||||
name={`customHeaders.${index}.value`}
|
||||
placeholder="Header Value"
|
||||
className="flex-1 p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => arrayHelpers.remove(index)}
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="transition-colors duration-200 hover:bg-red-600"
|
||||
>
|
||||
<Field
|
||||
name={`customHeaders.${index}.key`}
|
||||
placeholder="Header Key"
|
||||
className="flex-1 p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
/>
|
||||
<Field
|
||||
name={`customHeaders.${index}.value`}
|
||||
placeholder="Header Value"
|
||||
className="flex-1 p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => arrayHelpers.remove(index)}
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="transition-colors duration-200 hover:bg-red-600"
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
@@ -268,6 +278,75 @@ function ToolForm({
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="mt-6">
|
||||
<h3 className="text-xl font-bold mb-2 text-primary-600">
|
||||
Authentication
|
||||
</h3>
|
||||
{isOAuthEnabled ? (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div
|
||||
className={
|
||||
values.customHeaders.some(
|
||||
(header) =>
|
||||
header.key.toLowerCase() === "authorization"
|
||||
)
|
||||
? "opacity-50"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<Checkbox
|
||||
id="passthrough_auth"
|
||||
size="sm"
|
||||
checked={values.passthrough_auth}
|
||||
disabled={values.customHeaders.some(
|
||||
(header) =>
|
||||
header.key.toLowerCase() === "authorization" &&
|
||||
!values.passthrough_auth
|
||||
)}
|
||||
onCheckedChange={(checked) => {
|
||||
setFieldValue("passthrough_auth", checked, true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{values.customHeaders.some(
|
||||
(header) => header.key.toLowerCase() === "authorization"
|
||||
) && (
|
||||
<TooltipContent side="top" align="center">
|
||||
<p className="bg-background-900 max-w-[200px] mb-1 text-sm rounded-lg p-1.5 text-white">
|
||||
Cannot enable OAuth passthrough when an
|
||||
Authorization header is already set
|
||||
</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div className="flex flex-col">
|
||||
<label
|
||||
htmlFor="passthrough_auth"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Pass through user's OAuth token
|
||||
</label>
|
||||
<p className="text-xs text-subtle mt-1">
|
||||
When enabled, the user's OAuth token will be passed
|
||||
as the Authorization header for all API calls
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-subtle">
|
||||
OAuth passthrough is only available when OIDC or OAuth
|
||||
authentication is enabled
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -291,6 +370,7 @@ function ToolForm({
|
||||
interface ToolFormValues {
|
||||
definition: string;
|
||||
customHeaders: { key: string; value: string }[];
|
||||
passthrough_auth: boolean;
|
||||
}
|
||||
|
||||
const ToolSchema = Yup.object().shape({
|
||||
@@ -303,6 +383,7 @@ const ToolSchema = Yup.object().shape({
|
||||
})
|
||||
)
|
||||
.default([]),
|
||||
passthrough_auth: Yup.boolean().default(false),
|
||||
});
|
||||
|
||||
export function ToolEditor({ tool }: { tool?: ToolSnapshot }) {
|
||||
@@ -326,9 +407,27 @@ export function ToolEditor({ tool }: { tool?: ToolSnapshot }) {
|
||||
key: header.key,
|
||||
value: header.value,
|
||||
})) ?? [],
|
||||
passthrough_auth: tool?.passthrough_auth ?? false,
|
||||
}}
|
||||
validationSchema={ToolSchema}
|
||||
onSubmit={async (values: ToolFormValues) => {
|
||||
const hasAuthHeader = values.customHeaders?.some(
|
||||
(header) => header.key.toLowerCase() === "authorization"
|
||||
);
|
||||
if (hasAuthHeader && values.passthrough_auth) {
|
||||
setPopup({
|
||||
message:
|
||||
"Cannot enable passthrough auth when Authorization " +
|
||||
"headers are present. Please remove any Authorization " +
|
||||
"headers first.",
|
||||
type: "error",
|
||||
});
|
||||
console.log(
|
||||
"Cannot enable passthrough auth when Authorization headers are present. Please remove any Authorization headers first."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let definition: any;
|
||||
try {
|
||||
definition = parseJsonWithTrailingCommas(values.definition);
|
||||
@@ -344,6 +443,7 @@ export function ToolEditor({ tool }: { tool?: ToolSnapshot }) {
|
||||
description: description || "",
|
||||
definition: definition,
|
||||
custom_headers: values.customHeaders,
|
||||
passthrough_auth: values.passthrough_auth,
|
||||
};
|
||||
let response;
|
||||
if (tool) {
|
||||
|
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
import {
|
||||
ConnectorIndexingStatus,
|
||||
OAuthSlackCallbackResponse,
|
||||
DocumentBoostStatus,
|
||||
Tag,
|
||||
UserGroup,
|
||||
@@ -20,13 +19,10 @@ import { AllUsersResponse } from "./types";
|
||||
import { Credential } from "./connectors/credentials";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import { PersonaLabel } from "@/app/admin/assistants/interfaces";
|
||||
import {
|
||||
LLMProvider,
|
||||
LLMProviderDescriptor,
|
||||
} from "@/app/admin/configuration/llm/interfaces";
|
||||
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
|
||||
import { isAnthropic } from "@/app/admin/configuration/llm/interfaces";
|
||||
import { getSourceMetadata } from "./sources";
|
||||
import { buildFilters } from "./search/utils";
|
||||
import { AuthType, NEXT_PUBLIC_CLOUD_ENABLED } from "./constants";
|
||||
|
||||
const CREDENTIAL_URL = "/api/manage/admin/credential";
|
||||
|
||||
@@ -454,6 +450,23 @@ export function useLlmOverride(
|
||||
};
|
||||
}
|
||||
|
||||
export function useAuthType(): AuthType | null {
|
||||
const { data, error } = useSWR<{ auth_type: AuthType }>(
|
||||
"/api/auth/type",
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
if (NEXT_PUBLIC_CLOUD_ENABLED) {
|
||||
return "cloud";
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return data.auth_type;
|
||||
}
|
||||
|
||||
/*
|
||||
EE Only APIs
|
||||
*/
|
||||
|
@@ -11,6 +11,7 @@ export async function createCustomTool(toolData: {
|
||||
description?: string;
|
||||
definition: Record<string, any>;
|
||||
custom_headers: { key: string; value: string }[];
|
||||
passthrough_auth: boolean;
|
||||
}): Promise<ApiResponse<ToolSnapshot>> {
|
||||
try {
|
||||
const response = await fetch("/api/admin/tool/custom", {
|
||||
@@ -41,6 +42,7 @@ export async function updateCustomTool(
|
||||
description?: string;
|
||||
definition?: Record<string, any>;
|
||||
custom_headers: { key: string; value: string }[];
|
||||
passthrough_auth: boolean;
|
||||
}
|
||||
): Promise<ApiResponse<ToolSnapshot>> {
|
||||
try {
|
||||
|
@@ -13,6 +13,9 @@ export interface ToolSnapshot {
|
||||
|
||||
// only specified for Custom Tools. ID of the tool in the codebase.
|
||||
in_code_tool_id: string | null;
|
||||
|
||||
// whether to pass through the user's OAuth token as Authorization header
|
||||
passthrough_auth: boolean;
|
||||
}
|
||||
|
||||
export interface MethodSpec {
|
||||
|
Reference in New Issue
Block a user