feat(ui): configure OpenAI workload identity federation from the LLM Credentials and Add Model forms (#45528)

* feat(ui): configure OpenAI workload identity federation from the LLM Credentials and Add Model forms

* fix(ui): let a federated OpenAI credential omit the service account when the proxy env provides it

* fix(ui): clear the federation API Base error when the admin switches the credential to an API key
This commit is contained in:
Mateo Wang 2026-10-09 00:30:44 -07:00 • committed by GitHub
parent cdba36a1f0
commit a3236ba947
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 949 additions and 411 deletions

View file

@ -464,8 +464,32 @@ describe("AddModelForm", () => {
expect(providerSelect).toBeDisabled();
});
it("saves an OpenAI federated credential and attaches it to the model", async () => {
const user = userEvent.setup();
const props = await renderAsRole("proxy_admin", Providers.OpenAI);
await user.click(screen.getByRole("button", { name: "Use workload identity federation" }));
expect(await screen.findByRole("combobox", { name: /^Authentication:/ })).toHaveTextContent(
"Workload identity federation",
);
fill("Credential Name:", "openai-federated");
fill(/Service Account ID/, "svc_new");
await user.click(screen.getByRole("button", { name: "Add Credential" }));
await waitFor(() => {
expect(credentialCreateCall).toHaveBeenCalledWith("test-access-token", {
credential_name: "openai-federated",
credential_values: { openai_service_account_id: "svc_new" },
credential_info: { custom_llm_provider: Providers.OpenAI },
});
});
await waitFor(() => {
expect(props.form.getValues("litellm_credential_name")).toBe("openai-federated");
});
});
it("is not offered for a provider without federation support", async () => {
await renderAsRole("proxy_admin", Providers.OpenAI);
await renderAsRole("proxy_admin", Providers.OpenAI_Compatible);
expect(screen.queryByRole("button", { name: "Use workload identity federation" })).not.toBeInTheDocument();
});

View file

@ -27,7 +27,7 @@ import {
import type { Team } from "../key_team_helpers/key_list";
import { type CredentialItem, type ProviderCreateInfo, credentialCreateCall, modelAvailableCall } from "../networking";
import CredentialModal from "../model_add/CredentialModal";
import { isAnthropicProvider } from "../model_add/anthropic_federation";
import { federatedProviderOf } from "../model_add/credential_federation";
import { buildCredential, withoutRestrictedFields } from "../model_add/credential_form_helpers";
import { ProviderLogo } from "../molecules/models/ProviderLogo";
import AccessGroupTagsCombobox from "./AccessGroupTagsCombobox";
@ -100,7 +100,8 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
const selectedCredentialName = useWatch({ control: form.control, name: "litellm_credential_name" });
const queryClient = useQueryClient();
const [isFederatedCredentialModalOpen, setIsFederatedCredentialModalOpen] = useState(false);
const canCreateFederatedCredential = isProxyAdminRole(userRole ?? "") && isAnthropicProvider(selectedProvider);
const canCreateFederatedCredential =
isProxyAdminRole(userRole ?? "") && federatedProviderOf(selectedProvider) !== null;
const handleCreateFederatedCredential = async (values: Record<string, unknown>) => {
const credential = buildCredential(values, withoutRestrictedFields(values));

View file

@ -16,10 +16,12 @@ import {
import { ProviderCredentialFieldMetadata } from "../networking";
import { Providers } from "../provider_info_helpers";
import { labelWithHint } from "@/components/shared/form/LabelWithHint";
import type { ProviderFieldValidators } from "../model_add/credential_federation";
interface ProviderSpecificFieldsProps {
selectedProvider: string | null;
hiddenFieldKeys?: readonly string[];
fieldValidators?: ProviderFieldValidators;
}
const readTextFile = (file: File, onLoaded: (contents: string) => void) => {
@ -81,7 +83,11 @@ const mapFieldMetadataToUiField = (field: ProviderCredentialFieldMetadata): Prov
const providerFieldsByDisplayName: Record<string, ProviderCredentialField[]> = {};
const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({ selectedProvider, hiddenFieldKeys }) => {
const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({
selectedProvider,
hiddenFieldKeys,
fieldValidators,
}) => {
const selectedProviderEnum = Providers[selectedProvider as keyof typeof Providers] as Providers;
const form = useFormContext<MountedFormValues>();
const credentialsFileRef = React.useRef<HTMLInputElement>(null);
@ -297,7 +303,12 @@ const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({ selecte
label={field.tooltip ? labelWithHint(field.label, field.tooltip) : field.label}
name={field.key}
required={field.required}
rules={field.required ? { validate: { required: requiredRule("Required") } } : undefined}
rules={{
validate: {
...(field.required ? { required: requiredRule("Required") } : {}),
...(fieldValidators?.[field.key] ? { provider: fieldValidators[field.key] } : {}),
},
}}
className={field.key === "vertex_credentials" ? "mb-0" : "mb-4"}
>
{(control) => renderFieldControl(field, control)}

View file

@ -442,3 +442,143 @@ describe("CredentialModal with Anthropic workload identity federation", () => {
);
});
});
const openAIFederatedCredential: CredentialItem = {
credential_name: "openai-federated",
credential_values: {
api_base: "https://api.openai.com/v1",
openai_identity_provider_id: "idp_stored",
openai_service_account_id: "svc_stored",
openai_identity_token_file: "/var****",
},
credential_info: { custom_llm_provider: "openai" },
};
describe("CredentialModal with OpenAI workload identity federation", () => {
it("creates a federated OpenAI credential and never sends an API key", async () => {
const user = userEvent.setup();
const onSubmit = renderModal({ initialProvider: "OpenAI" });
await screen.findByLabelText("OpenAI API Key");
fill("OpenAI API Key", "sk-proj-typed-before-switching");
await chooseOption(user, /^Authentication:/, "Workload identity federation");
expect(screen.queryByLabelText("OpenAI API Key")).not.toBeInTheDocument();
expect(screen.getByText(/only when OPENAI_API_KEY is unset/)).toBeInTheDocument();
fill("Credential Name:", "openai-federated");
fill(/Identity Provider ID/, "idp_new");
fill(/Service Account ID/, " svc_new ");
fill(/Identity Token File/, "/var/run/secrets/kubernetes.io/serviceaccount/token");
await user.click(screen.getByRole("button", { name: "Add Credential" }));
const expectedPayload = {
credential_name: "openai-federated",
custom_llm_provider: "OpenAI",
openai_identity_provider_id: "idp_new",
openai_service_account_id: "svc_new",
openai_identity_token_file: "/var/run/secrets/kubernetes.io/serviceaccount/token",
};
expect(onSubmit).toHaveBeenCalledWith(expectedPayload, []);
});
it("saves a stored federated OpenAI credential untouched when its service account comes from the proxy environment", async () => {
const user = userEvent.setup();
const { openai_service_account_id: _, ...valuesWithoutServiceAccount } =
openAIFederatedCredential.credential_values;
const onSubmit = renderModal({
mode: "edit",
existingCredential: { ...openAIFederatedCredential, credential_values: valuesWithoutServiceAccount },
});
await screen.findByLabelText(/Service Account ID/);
await user.click(screen.getByRole("button", { name: "Update Credential" }));
expect(onSubmit).toHaveBeenCalledWith({ credential_name: "openai-federated", custom_llm_provider: "openai" }, []);
});
it("refuses a base URL the proxy would not federate with, and drops that check once the admin picks an API key", async () => {
const user = userEvent.setup();
const onSubmit = renderModal({ initialProvider: "OpenAI", initialAuthMethod: "federation" });
await screen.findByLabelText("API Base");
fill("Credential Name:", "openai-gateway");
fill(/Service Account ID/, "svc_new");
fill("API Base", "https://gateway.example.com/v1");
await user.click(screen.getByRole("button", { name: "Add Credential" }));
expect(await screen.findByText(/only reaches the OpenAI API/)).toBeInTheDocument();
expect(onSubmit).not.toHaveBeenCalled();
await chooseOption(user, /^Authentication:/, "API key");
await screen.findByLabelText("OpenAI API Key");
expect(screen.queryByText(/only reaches the OpenAI API/)).not.toBeInTheDocument();
fill("OpenAI API Key", "sk-proj-new");
await user.click(screen.getByRole("button", { name: "Add Credential" }));
const expectedPayload = {
credential_name: "openai-gateway",
custom_llm_provider: "OpenAI",
api_base: "https://gateway.example.com/v1",
api_key: "sk-proj-new",
};
expect(onSubmit).toHaveBeenCalledWith(expectedPayload, []);
});
it("shows a stored federated OpenAI credential and writes nothing when it is saved untouched", async () => {
const user = userEvent.setup();
const onSubmit = renderModal({ mode: "edit", existingCredential: openAIFederatedCredential });
expect(await screen.findByRole("combobox", { name: /^Authentication:/ })).toHaveTextContent(
"Workload identity federation",
);
expect(screen.getByLabelText(/Service Account ID/)).toHaveValue("svc_stored");
expect(screen.getByLabelText(/Identity Token File/)).toHaveValue("/var****");
expect(screen.queryByLabelText("OpenAI API Key")).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Update Credential" }));
expect(onSubmit).toHaveBeenCalledWith({ credential_name: "openai-federated", custom_llm_provider: "openai" }, []);
});
it("deletes the stored OpenAI federation values when the admin switches the credential to an API key", async () => {
const user = userEvent.setup();
const onSubmit = renderModal({ mode: "edit", existingCredential: openAIFederatedCredential });
await screen.findByLabelText(/Service Account ID/);
await chooseOption(user, /^Authentication:/, "API key");
await screen.findByLabelText("OpenAI API Key");
fill("OpenAI API Key", "sk-proj-replacement");
await user.click(screen.getByRole("button", { name: "Update Credential" }));
const [values, valuesToDelete] = onSubmit.mock.calls[0];
expect(values).toEqual({
credential_name: "openai-federated",
custom_llm_provider: "openai",
api_key: "sk-proj-replacement",
});
expect([...valuesToDelete].sort()).toEqual([
"openai_identity_provider_id",
"openai_identity_token_file",
"openai_service_account_id",
]);
});
it("restores the stored federation settings when the admin returns to the credential's own provider", async () => {
const user = userEvent.setup();
const onSubmit = renderModal({ mode: "edit", existingCredential: federatedCredential });
await screen.findByLabelText(/Federation Rule ID/);
await chooseProvider(user, "OpenAI");
expect(await screen.findByRole("combobox", { name: /^Authentication:/ })).toHaveTextContent("API key");
await chooseProvider(user, "Anthropic");
expect(await screen.findByRole("combobox", { name: /Identity Source/ })).toHaveTextContent(
"Keycloak client credentials",
);
await user.click(screen.getByRole("button", { name: "Update Credential" }));
expect(onSubmit).toHaveBeenCalledWith(
{ credential_name: "anthropic-federated", custom_llm_provider: "Anthropic" },
[],
);
});
});

View file

@ -19,18 +19,19 @@ import { Providers } from "../provider_info_helpers";
import { Logo } from "@/components/molecules/logo/Logo";
import { resetCredentialFormOnProviderChange, withoutRestrictedFields } from "./credential_form_helpers";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import AnthropicFederationFields from "./AnthropicFederationFields";
import FederationFields from "./FederationFields";
import { DEFAULT_IDENTITY_SOURCE, inferIdentitySource, type IdentitySourceId } from "./anthropic_federation";
import {
buildCreateCredentialValues,
buildCredentialPatch,
buildProviderChangePatch,
federatedProviderOf,
inferAuthMethod,
inferIdentitySource,
isAnthropicProvider,
isFederatedCredential,
type AnthropicAuthMethod,
type IdentitySourceId,
} from "./anthropic_federation";
providerFieldValidators,
selectionFor,
type AuthMethod,
} from "./credential_federation";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
const providerOptions: SearchSelectOption[] = Object.entries(Providers).map(([providerEnum, providerDisplayName]) => ({
@ -39,11 +40,11 @@ const providerOptions: SearchSelectOption[] = Object.entries(Providers).map(([pr
icon: <Logo provider={providerEnum} label={providerDisplayName} className="w-5 h-5" />,
}));
const AUTH_METHOD_SELECT_ID = "anthropic_auth_method";
const AUTH_METHOD_SELECT_ID = "credential_auth_method";
const API_KEY_FIELDS: readonly string[] = ["api_key"];
const NO_HIDDEN_FIELDS: readonly string[] = [];
const authMethodItems: { value: AnthropicAuthMethod; label: string }[] = [
const authMethodItems: { value: AuthMethod; label: string }[] = [
{ value: "api_key", label: "API key" },
{ value: "federation", label: "Workload identity federation" },
];
@ -55,7 +56,7 @@ interface CredentialModalProps {
mode: "add" | "edit";
existingCredential?: CredentialItem | null;
initialProvider?: string | null;
initialAuthMethod?: AnthropicAuthMethod;
initialAuthMethod?: AuthMethod;
providerLocked?: boolean;
}
@ -111,18 +112,16 @@ export default function CredentialModal({
);
const storedProvider = existingCredential?.credential_info.custom_llm_provider ?? null;
const storedValues: Record<string, unknown> = existingCredential?.credential_values ?? {};
const storedSelection = {
authMethod: inferAuthMethod(storedValues),
identitySource: inferIdentitySource(storedValues),
};
const [authMethod, setAuthMethod] = useState<AnthropicAuthMethod>(
existingCredential ? storedSelection.authMethod : initialAuthMethod ?? "api_key",
const storedAuthMethod = inferAuthMethod(storedValues);
const storedIdentitySource = isFederatedCredential(storedValues)
? inferIdentitySource(storedValues)
: DEFAULT_IDENTITY_SOURCE;
const storedSelection = selectionFor(federatedProviderOf(storedProvider), storedAuthMethod, storedIdentitySource);
const [authMethod, setAuthMethod] = useState<AuthMethod>(
existingCredential ? storedAuthMethod : initialAuthMethod ?? "api_key",
);
const [identitySource, setIdentitySource] = useState<IdentitySourceId>(
isFederatedCredential(storedValues) ? storedSelection.identitySource : "token_file",
);
const isAnthropic = isAnthropicProvider(selectedProvider);
const selection = { authMethod: isAnthropic ? authMethod : ("api_key" as const), identitySource };
const [identitySource, setIdentitySource] = useState<IdentitySourceId>(storedIdentitySource);
const selection = selectionFor(federatedProviderOf(selectedProvider), authMethod, identitySource);
const initialValues = initialFormValues(existingCredential, initialProvider);
@ -135,6 +134,18 @@ export default function CredentialModal({
setFieldValue: (field: string, value: unknown) => form.setValue(field, value),
});
const changeProvider = (provider: string | null) => {
const backToStored = isEdit && sameProvider(provider, storedProvider);
setAuthMethod(backToStored ? storedAuthMethod : "api_key");
setIdentitySource(backToStored ? storedIdentitySource : DEFAULT_IDENTITY_SOURCE);
resetCredentialFormOnProviderChange(formAdapterFor(provider), provider, setSelectedProvider);
};
const changeAuthMethod = (method: AuthMethod) => {
form.clearErrors(Object.keys(providerFieldValidators(selection)));
setAuthMethod(method);
};
const handleSubmit = async () => {
const isValid = await form.trigger(registry.mountedNames() as string[]);
if (!isValid) {
@ -234,24 +245,24 @@ export default function CredentialModal({
disabled={providerLocked}
onValueChange={(value) => {
control.onChange(value);
resetCredentialFormOnProviderChange(formAdapterFor(value), value, setSelectedProvider);
changeProvider(value);
}}
/>
)}
</MountedFormField>
{isAnthropic && (
{federatedProviderOf(selectedProvider) !== null && (
<div className="mb-4 flex flex-col gap-2">
<label htmlFor={AUTH_METHOD_SELECT_ID} className="text-sm font-medium">
{labelWithHint(
"Authentication:",
"Workload identity federation exchanges an identity token for a short-lived Anthropic access token, so no API key is stored.",
"Workload identity federation exchanges an identity token for a short-lived access token from the provider, so no API key is stored.",
)}
</label>
<Select
items={authMethodItems}
value={authMethod}
onValueChange={(value) => setAuthMethod(value as AnthropicAuthMethod)}
onValueChange={(value) => changeAuthMethod(value as AuthMethod)}
>
<SelectTrigger id={AUTH_METHOD_SELECT_ID} className="w-full">
<SelectValue />
@ -270,11 +281,12 @@ export default function CredentialModal({
<ProviderSpecificFields
selectedProvider={selectedProvider}
hiddenFieldKeys={selection.authMethod === "federation" ? API_KEY_FIELDS : NO_HIDDEN_FIELDS}
fieldValidators={providerFieldValidators(selection)}
/>
{selection.authMethod === "federation" && (
<AnthropicFederationFields
identitySource={identitySource}
<FederationFields
selection={selection}
onIdentitySourceChange={setIdentitySource}
storedValues={storedValues}
/>

View file

@ -63,7 +63,7 @@ describe("CredentialsTable", () => {
expect(screen.getByText("Azure")).toBeInTheDocument();
});
it("should mark only the credential that stores federation values as federated", () => {
it("should mark only the credentials the proxy federates as federated", () => {
const credentials: CredentialItem[] = [
{
credential_name: "a-anthropic-federated",
@ -75,12 +75,23 @@ describe("CredentialsTable", () => {
credential_values: { api_key: "sk-a****" },
credential_info: { custom_llm_provider: "anthropic" },
},
{
credential_name: "c-openai-federated",
credential_values: { openai_service_account_id: "svc_stored" },
credential_info: { custom_llm_provider: "openai" },
},
{
credential_name: "d-openai-key-and-federation",
credential_values: { api_key: "sk-p****", openai_service_account_id: "svc_stored" },
credential_info: { custom_llm_provider: "openai" },
},
];
render(<CredentialsTable {...defaultProps} credentials={credentials} />);
const [federatedRow, apiKeyRow] = screen.getAllByRole("row").slice(1);
expect(within(federatedRow).getByText("a-anthropic-federated")).toBeInTheDocument();
expect(within(federatedRow).getByText("Workload identity federation")).toBeInTheDocument();
expect(within(apiKeyRow).queryByText("Workload identity federation")).not.toBeInTheDocument();
const rows = screen.getAllByRole("row").slice(1);
const federatedNames = rows
.filter((row) => within(row).queryByText("Workload identity federation") !== null)
.map((row) => within(row).getAllByRole("cell")[0].textContent);
expect(federatedNames).toEqual(["a-anthropic-federated", "c-openai-federated"]);
});
it("should render a dash when a credential has no provider", () => {

View file

@ -22,7 +22,7 @@ import {
import { cn } from "@/lib/cva.config";
import { copyToClipboard } from "@/utils/dataUtils";
import { isFederatedCredential } from "./anthropic_federation";
import { inferAuthMethod } from "./credential_federation";
function CredentialProviderCell({ provider, federated }: { provider: string | undefined; federated: boolean }) {
if (!provider) {
@ -144,7 +144,7 @@ export const getCredentialsTableColumns = ({
cell: ({ row }) => (
<CredentialProviderCell
provider={row.original.credential_info?.custom_llm_provider}
federated={isFederatedCredential(row.original.credential_values)}
federated={inferAuthMethod(row.original.credential_values) === "federation"}
/>
),
},

View file

@ -7,17 +7,19 @@ import {
FEDERATION_CORE_FIELDS,
identitySourceById,
identitySourceOptions,
requiredFederationValue,
validateFederationValueStored,
validateIdentityTokenReference,
validateIssuerTtlSeconds,
validateMaskedValueUntouched,
type FederationField,
type IdentitySourceId,
} from "./anthropic_federation";
import type { FederatedSelection } from "./credential_federation";
import { requiredFederationValue, validateMaskedValueUntouched, type FederationField } from "./federation_field";
import { OPENAI_FEDERATION_FIELDS } from "./openai_federation";
interface AnthropicFederationFieldsProps {
identitySource: IdentitySourceId;
type FieldValidator = (value: unknown, formValues: Record<string, unknown>) => string | true;
interface FederationFieldsProps {
selection: FederatedSelection;
onIdentitySourceChange: (identitySource: IdentitySourceId) => void;
storedValues: Record<string, unknown>;
}
@ -25,14 +27,13 @@ interface AnthropicFederationFieldsProps {
const IDENTITY_SOURCE_SELECT_ID = "anthropic_federation_identity_source";
const STORED_VALUE_MESSAGE_FIELD_KEY = FEDERATION_CORE_FIELDS[0].key;
const fieldRules = (field: FederationField, storedValue: unknown, identitySource: IdentitySourceId) => ({
validate: {
...(field.required ? { required: requiredFederationValue } : {}),
...(field.key === STORED_VALUE_MESSAGE_FIELD_KEY ? { stored: validateFederationValueStored(identitySource) } : {}),
...(field.key === "anthropic_identity_token" ? { reference: validateIdentityTokenReference } : {}),
...(field.control === "integer" ? { ttl: validateIssuerTtlSeconds } : {}),
masked: validateMaskedValueUntouched(storedValue),
},
const anthropicValidators = (
field: FederationField,
identitySource: IdentitySourceId,
): Readonly<Record<string, FieldValidator>> => ({
...(field.key === STORED_VALUE_MESSAGE_FIELD_KEY ? { stored: validateFederationValueStored(identitySource) } : {}),
...(field.key === "anthropic_identity_token" ? { reference: validateIdentityTokenReference } : {}),
...(field.control === "integer" ? { ttl: validateIssuerTtlSeconds } : {}),
});
const selectItems = (field: FederationField, value: unknown) => [
@ -76,26 +77,52 @@ const renderControl = (field: FederationField, control: MountedFieldControlProps
);
};
export default function AnthropicFederationFields({
identitySource,
onIdentitySourceChange,
storedValues,
}: AnthropicFederationFieldsProps) {
const sourceFields = identitySourceById(identitySource).fields;
const identitySourceItems = identitySourceOptions(storedValues);
interface FederationFieldInputProps {
field: FederationField;
storedValue: unknown;
validators?: Readonly<Record<string, FieldValidator>>;
}
const renderField = (field: FederationField) => (
function FederationFieldInput({ field, storedValue, validators }: FederationFieldInputProps) {
return (
<MountedFormField
key={field.key}
label={labelWithHint(field.label, field.tooltip)}
name={field.key}
required={field.required}
rules={fieldRules(field, storedValues[field.key], identitySource)}
rules={{
validate: {
...(field.required ? { required: requiredFederationValue } : {}),
...validators,
masked: validateMaskedValueUntouched(storedValue),
},
}}
className="mb-4"
>
{(control) => renderControl(field, control)}
</MountedFormField>
);
}
interface AnthropicFederationFieldsProps {
identitySource: IdentitySourceId;
onIdentitySourceChange: (identitySource: IdentitySourceId) => void;
storedValues: Record<string, unknown>;
}
function AnthropicFederationFields({
identitySource,
onIdentitySourceChange,
storedValues,
}: AnthropicFederationFieldsProps) {
const identitySourceItems = identitySourceOptions(storedValues);
const renderField = (field: FederationField) => (
<FederationFieldInput
key={field.key}
field={field}
storedValue={storedValues[field.key]}
validators={anthropicValidators(field, identitySource)}
/>
);
return (
<>
@ -135,7 +162,36 @@ export default function AnthropicFederationFields({
</p>
)}
</div>
{sourceFields.map(renderField)}
{identitySourceById(identitySource).fields.map(renderField)}
</>
);
}
function OpenAIFederationFields({ storedValues }: { storedValues: Record<string, unknown> }) {
return (
<>
<p className="mb-4 text-sm text-muted-foreground">
The proxy exchanges the identity token for an OpenAI access token only when OPENAI_API_KEY is unset in its
environment. Otherwise it sends that key instead.
</p>
{OPENAI_FEDERATION_FIELDS.map((field) => (
<FederationFieldInput key={field.key} field={field} storedValue={storedValues[field.key]} />
))}
</>
);
}
export default function FederationFields({ selection, onIdentitySourceChange, storedValues }: FederationFieldsProps) {
switch (selection.provider) {
case "anthropic":
return (
<AnthropicFederationFields
identitySource={selection.identitySource}
onIdentitySourceChange={onIdentitySourceChange}
storedValues={storedValues}
/>
);
case "openai":
return <OpenAIFederationFields storedValues={storedValues} />;
}
}

View file

@ -1,34 +1,14 @@
import { describe, expect, it } from "vitest";
import {
buildCreateCredentialValues,
buildCredentialPatch,
buildProviderChangePatch,
inferAuthMethod,
identitySourceOptions,
inferIdentitySource,
isAnthropicProvider,
isFederatedCredential,
MAX_ISSUER_TTL_SECONDS,
validateFederationValueStored,
validateIdentityTokenReference,
validateIssuerTtlSeconds,
validateMaskedValueUntouched,
} from "./anthropic_federation";
const apiKeySelection = { authMethod: "api_key", identitySource: "token_file" } as const;
const tokenFileSelection = { authMethod: "federation", identitySource: "token_file" } as const;
const internalIssuerSelection = { authMethod: "federation", identitySource: "internal_issuer" } as const;
const keycloakSelection = { authMethod: "federation", identitySource: "keycloak" } as const;
const environmentSelection = { authMethod: "federation", identitySource: "environment" } as const;
const storedTokenFile = {
api_base: "https://api.anthropic.com",
anthropic_federation_rule_id: "fdrl_stored",
anthropic_organization_id: "org-stored",
anthropic_federation_workspace_id: "wrkspc_stored",
anthropic_identity_token_file: "/var****",
};
const storedKeycloak = {
anthropic_federation_rule_id: "fdrl_stored",
anthropic_organization_id: "org-stored",
@ -50,18 +30,7 @@ describe("isAnthropicProvider", () => {
});
});
describe("reading a stored credential", () => {
it("treats a credential with any federation value as federated", () => {
expect(isFederatedCredential({ anthropic_federation_rule_id: "fdrl_1" })).toBe(true);
expect(inferAuthMethod({ anthropic_identity_source: "keycloak" })).toBe("federation");
});
it("treats an api key credential, an empty one, and a missing one as not federated", () => {
expect(isFederatedCredential({ api_key: "sk-1****", api_base: "https://api.anthropic.com" })).toBe(false);
expect(isFederatedCredential({ anthropic_federation_rule_id: "" })).toBe(false);
expect(inferAuthMethod(undefined)).toBe("api_key");
});
describe("reading a stored identity source", () => {
it("lets a declared identity source win over a leftover token file", () => {
expect(
inferIdentitySource({ anthropic_identity_source: "internal_issuer", anthropic_identity_token_file: "/var****" }),
@ -88,11 +57,6 @@ describe("reading a stored credential", () => {
});
expect(identitySourceOptions(storedKeycloak).map((option) => option.value)).not.toContain("unrecognized");
});
it("opens a credential that stores an api key next to federation values as an api key credential", () => {
expect(inferAuthMethod({ api_key: "sk-1****", anthropic_federation_rule_id: "fdrl_1" })).toBe("api_key");
expect(inferAuthMethod({ api_key: "", anthropic_federation_rule_id: "fdrl_1" })).toBe("federation");
});
});
describe("field validation", () => {
@ -123,13 +87,6 @@ describe("field validation", () => {
expect(validateIssuerTtlSeconds(ttl)).toEqual(expect.stringContaining("whole number"));
});
it("refuses a hidden stored value that was only partly edited", () => {
const rule = validateMaskedValueUntouched("os.e****");
expect(rule("os.e****")).toBe(true);
expect(rule("os.environ/NEW_REF")).toBe(true);
expect(rule("os.e****_NEW")).toEqual(expect.stringContaining("Replace the whole value"));
});
it("refuses the proxy environment source when every id is blank, since the proxy rejects a credential with no values", () => {
const rule = validateFederationValueStored("environment");
const blankIds = {
@ -143,193 +100,3 @@ describe("field validation", () => {
expect(validateFederationValueStored("token_file")("", blankIds)).toBe(true);
});
});
describe("buildCreateCredentialValues", () => {
it("sends the typed federation values and nothing for the fields left empty", () => {
const typedValues = {
api_base: "",
anthropic_federation_rule_id: " fdrl_new\n",
anthropic_organization_id: "org-new",
anthropic_service_account_id: "",
anthropic_federation_workspace_id: undefined,
anthropic_identity_token_file: "/var/run/secrets/anthropic/token",
};
expect(buildCreateCredentialValues(typedValues, tokenFileSelection)).toEqual({
anthropic_federation_rule_id: "fdrl_new",
anthropic_organization_id: "org-new",
anthropic_identity_token_file: "/var/run/secrets/anthropic/token",
});
});
it("names the identity source and sends the lifetime as a number for the internal issuer", () => {
const typedValues = {
anthropic_federation_rule_id: "fdrl_new",
anthropic_organization_id: "org-new",
anthropic_issuer_url: "https://litellm.example.com",
anthropic_issuer_subject: "litellm-proxy",
anthropic_issuer_ttl_seconds: "120",
anthropic_issuer_signing_key_ref: "os.environ/ISSUER_KEY",
};
const values = buildCreateCredentialValues(typedValues, internalIssuerSelection);
expect(values.anthropic_identity_source).toBe("internal_issuer");
expect(values.anthropic_issuer_ttl_seconds).toBe(120);
});
it("does not name an identity source for an api key credential and keeps its values as typed", () => {
expect(buildCreateCredentialValues({ api_key: " sk-ant-typed ", api_base: "" }, apiKeySelection)).toEqual({
api_key: " sk-ant-typed ",
});
});
});
describe("buildCredentialPatch", () => {
it("writes nothing when the admin saves a federated credential untouched", () => {
expect(
buildCredentialPatch(storedTokenFile, { ...storedTokenFile }, tokenFileSelection, tokenFileSelection),
).toEqual({ credential_values: {}, credential_values_to_delete: [] });
expect(buildCredentialPatch(storedKeycloak, { ...storedKeycloak }, keycloakSelection, keycloakSelection)).toEqual({
credential_values: {},
credential_values_to_delete: [],
});
});
it("sends only the value the admin changed", () => {
expect(
buildCredentialPatch(
storedTokenFile,
{ ...storedTokenFile, anthropic_organization_id: "org-edited" },
tokenFileSelection,
tokenFileSelection,
),
).toEqual({ credential_values: { anthropic_organization_id: "org-edited" }, credential_values_to_delete: [] });
});
it("replaces a hidden stored value when the admin types a new one", () => {
const patch = buildCredentialPatch(
storedTokenFile,
{ ...storedTokenFile, anthropic_identity_token_file: "/run/secrets/new-token" },
tokenFileSelection,
tokenFileSelection,
);
expect(patch.credential_values).toEqual({ anthropic_identity_token_file: "/run/secrets/new-token" });
});
it("deletes every stored value the admin cleared, a base URL included", () => {
expect(
buildCredentialPatch(
{ ...storedTokenFile, api_base: "https://gateway.example.com" },
{ ...storedTokenFile, api_base: "", anthropic_federation_workspace_id: " " },
tokenFileSelection,
tokenFileSelection,
),
).toEqual({
credential_values: {},
credential_values_to_delete: ["api_base", "anthropic_federation_workspace_id"],
});
});
it("deletes every stored value the admin did not re-enter when the provider changed", () => {
const stored = { api_base: "https://corp.openai.azure.com", api_version: "2024-10-21", api_key: "sk-1****" };
const typed = { anthropic_federation_rule_id: "fdrl_1", anthropic_identity_token_file: "/run/secrets/token" };
expect(buildProviderChangePatch(stored, typed, tokenFileSelection)).toEqual({
credential_values: typed,
credential_values_to_delete: ["api_base", "api_version", "api_key"],
});
});
it("does not treat an unchanged stored lifetime as an edit", () => {
const stored = {
...storedTokenFile,
anthropic_identity_source: "internal_issuer",
anthropic_issuer_ttl_seconds: 300,
};
const patch = buildCredentialPatch(
stored,
{ anthropic_issuer_ttl_seconds: "300" },
internalIssuerSelection,
internalIssuerSelection,
);
expect(patch.credential_values).toEqual({});
});
it("drops the other source's stored values when the admin switches identity source", () => {
const typedValues = {
anthropic_federation_rule_id: "fdrl_stored",
anthropic_organization_id: "org-stored",
anthropic_issuer_url: "https://litellm.example.com",
anthropic_issuer_subject: "litellm-proxy",
anthropic_issuer_signing_key_ref: "os.environ/ISSUER_KEY",
};
const patch = buildCredentialPatch(storedKeycloak, typedValues, keycloakSelection, internalIssuerSelection);
const expectedValues = {
anthropic_identity_source: "internal_issuer",
anthropic_issuer_url: "https://litellm.example.com",
anthropic_issuer_subject: "litellm-proxy",
anthropic_issuer_signing_key_ref: "os.environ/ISSUER_KEY",
};
expect(patch.credential_values).toEqual(expectedValues);
expect([...patch.credential_values_to_delete].sort()).toEqual([
"anthropic_keycloak_auth_method",
"anthropic_keycloak_client_id",
"anthropic_keycloak_client_secret_ref",
"anthropic_keycloak_scope",
"anthropic_keycloak_token_url",
]);
});
it("drops the declared source when the admin switches to a token file or the proxy environment", () => {
const toEnvironment = buildCredentialPatch(storedKeycloak, {}, keycloakSelection, environmentSelection);
expect(toEnvironment.credential_values).toEqual({});
expect(toEnvironment.credential_values_to_delete).toContain("anthropic_identity_source");
expect(toEnvironment.credential_values_to_delete).not.toContain("anthropic_federation_rule_id");
});
it("deletes the stored api key when the admin switches the credential to federation", () => {
const typedValues = {
api_base: "https://api.anthropic.com",
anthropic_federation_rule_id: "fdrl_new",
anthropic_organization_id: "org-new",
anthropic_identity_token_file: "/var/run/secrets/anthropic/token",
};
const patch = buildCredentialPatch(
{ api_key: "sk-a****", api_base: "https://api.anthropic.com" },
typedValues,
apiKeySelection,
tokenFileSelection,
);
expect(patch.credential_values_to_delete).toEqual(["api_key"]);
expect(patch.credential_values).toEqual({
anthropic_federation_rule_id: "fdrl_new",
anthropic_organization_id: "org-new",
anthropic_identity_token_file: "/var/run/secrets/anthropic/token",
});
});
it("deletes every stored federation value when the admin switches the credential to an api key", () => {
const patch = buildCredentialPatch(storedKeycloak, { api_key: "sk-ant-new" }, keycloakSelection, apiKeySelection);
expect(patch.credential_values).toEqual({ api_key: "sk-ant-new" });
expect([...patch.credential_values_to_delete].sort()).toEqual(Object.keys(storedKeycloak).sort());
});
it("leaves a stored api key alone when the admin keeps a federated credential federated", () => {
const stored = { ...storedTokenFile, api_key: "sk-a****" };
const patch = buildCredentialPatch(
stored,
{ ...storedTokenFile, anthropic_organization_id: "org-edited" },
tokenFileSelection,
tokenFileSelection,
);
expect(patch.credential_values_to_delete).toEqual([]);
});
it("never names one key as both a write and a delete", () => {
const patch = buildCredentialPatch(
storedKeycloak,
{ ...storedKeycloak, anthropic_identity_token_file: "/var/run/secrets/anthropic/token" },
keycloakSelection,
tokenFileSelection,
);
const written = Object.keys(patch.credential_values);
expect(patch.credential_values_to_delete.filter((key) => written.includes(key))).toEqual([]);
});
});

View file

@ -1,6 +1,5 @@
import { isMaskedSecret } from "@/utils/maskedSecretUtils";
export type AnthropicAuthMethod = "api_key" | "federation";
import { isBlank, type FederationField } from "./federation_field";
export type IdentitySourceId =
| "token_file"
@ -10,16 +9,6 @@ export type IdentitySourceId =
| "environment"
| "unrecognized";
export interface FederationField {
readonly key: string;
readonly label: string;
readonly tooltip: string;
readonly placeholder?: string;
readonly required: boolean;
readonly control: "text" | "integer" | "select";
readonly options?: readonly string[];
}
export interface IdentitySource {
readonly id: IdentitySourceId;
readonly label: string;
@ -27,15 +16,9 @@ export interface IdentitySource {
readonly fields: readonly FederationField[];
}
export interface CredentialValuesPatch {
readonly credential_values: Record<string, unknown>;
readonly credential_values_to_delete: readonly string[];
}
export const MAX_ISSUER_TTL_SECONDS = 3600;
const IDENTITY_SOURCE_KEY = "anthropic_identity_source";
const API_KEY = "api_key";
const ACCEPTED_REFERENCE_PREFIX = "oidc/";
const REJECTED_REFERENCE_PREFIX = "oidc/env_path/";
@ -208,37 +191,31 @@ export const IDENTITY_SOURCES: readonly IdentitySource[] = [
},
];
export const DEFAULT_IDENTITY_SOURCE: IdentitySourceId = "token_file";
const IDENTITY_SOURCE_VALUE_KEYS: readonly string[] = [
IDENTITY_SOURCE_KEY,
...IDENTITY_SOURCES.flatMap((source) => source.fields.map((field) => field.key)),
];
export const FEDERATION_VALUE_KEYS: readonly string[] = [
export const ANTHROPIC_FEDERATION_FIELDS: readonly FederationField[] = [
...FEDERATION_CORE_FIELDS,
...IDENTITY_SOURCES.flatMap((source) => source.fields),
];
export const ANTHROPIC_FEDERATION_VALUE_KEYS: readonly string[] = [
...FEDERATION_CORE_FIELDS.map((field) => field.key),
...IDENTITY_SOURCE_VALUE_KEYS,
];
const UNRECOGNIZED_IDENTITY_SOURCE: IdentitySource = { id: "unrecognized", label: "", fixedValues: {}, fields: [] };
const isBlank = (value: unknown): boolean => {
if (typeof value === "string") {
return value.trim() === "";
}
return value === undefined || value === null;
};
export const identitySourceById = (id: IdentitySourceId): IdentitySource =>
IDENTITY_SOURCES.find((source) => source.id === id) ?? UNRECOGNIZED_IDENTITY_SOURCE;
export const isAnthropicProvider = (provider: string | null | undefined): boolean =>
provider !== null && provider !== undefined && provider.toLowerCase() === "anthropic";
export const isFederatedCredential = (credentialValues: Record<string, unknown> | null | undefined): boolean =>
FEDERATION_VALUE_KEYS.some((key) => !isBlank(credentialValues?.[key]));
export const inferAuthMethod = (credentialValues: Record<string, unknown> | null | undefined): AnthropicAuthMethod =>
isBlank(credentialValues?.[API_KEY]) && isFederatedCredential(credentialValues) ? "federation" : "api_key";
export const inferIdentitySource = (credentialValues: Record<string, unknown> | null | undefined): IdentitySourceId => {
const values = credentialValues ?? {};
const declared = values[IDENTITY_SOURCE_KEY];
@ -265,8 +242,6 @@ export const identitySourceOptions = (
...IDENTITY_SOURCES.map((source) => ({ value: source.id, label: source.label })),
];
export const requiredFederationValue = (value: unknown): string | true => (isBlank(value) ? "Required" : true);
export const validateFederationValueStored =
(identitySource: IdentitySourceId) =>
(_value: unknown, formValues: Record<string, unknown>): string | true =>
@ -293,87 +268,8 @@ export const validateIssuerTtlSeconds = (value: unknown): string | true => {
: `Enter a whole number of seconds from 1 to ${MAX_ISSUER_TTL_SECONDS}`;
};
export const validateMaskedValueUntouched =
(storedValue: unknown) =>
(value: unknown): string | true =>
isMaskedSecret(value) && value !== storedValue
? "This stored value is hidden. Replace the whole value to change it"
: true;
const toStoredType = (field: FederationField | undefined, value: unknown): unknown => {
if (field === undefined || isBlank(value)) {
return field === undefined ? value : "";
}
if (field.control === "integer") {
return Number(value);
}
return typeof value === "string" ? value.trim() : value;
};
const federationFieldsByKey: ReadonlyMap<string, FederationField> = new Map(
[...FEDERATION_CORE_FIELDS, ...IDENTITY_SOURCES.flatMap((source) => source.fields)].map((field) => [
field.key,
field,
]),
);
interface CredentialSelection {
readonly authMethod: AnthropicAuthMethod;
readonly identitySource: IdentitySourceId;
}
const fixedValuesFor = (selection: CredentialSelection): Readonly<Record<string, string>> =>
selection.authMethod === "federation" ? identitySourceById(selection.identitySource).fixedValues : {};
const typedFormValues = (formValues: Record<string, unknown>): Record<string, unknown> =>
Object.fromEntries(
Object.entries(formValues)
.map(([key, value]) => [key, toStoredType(federationFieldsByKey.get(key), value)] as const)
.filter(([, value]) => value !== "" && value !== undefined && value !== null),
);
export const buildCreateCredentialValues = (
formValues: Record<string, unknown>,
selection: CredentialSelection,
): Record<string, unknown> => ({ ...typedFormValues(formValues), ...fixedValuesFor(selection) });
const keysLeftBehind = (initial: CredentialSelection, selection: CredentialSelection): readonly string[] => {
if (selection.authMethod !== initial.authMethod) {
return selection.authMethod === "federation" ? [API_KEY] : FEDERATION_VALUE_KEYS;
}
if (selection.authMethod !== "federation" || selection.identitySource === initial.identitySource) {
return [];
}
const source = identitySourceById(selection.identitySource);
export const otherIdentitySourceKeys = (identitySource: IdentitySourceId): readonly string[] => {
const source = identitySourceById(identitySource);
const kept = new Set([...source.fields.map((field) => field.key), ...Object.keys(source.fixedValues)]);
return IDENTITY_SOURCE_VALUE_KEYS.filter((key) => !kept.has(key));
};
const changedValues = (stored: Record<string, unknown>, desired: Record<string, unknown>): Record<string, unknown> =>
Object.fromEntries(Object.entries(desired).filter(([key, value]) => !isMaskedSecret(value) && value !== stored[key]));
export const buildProviderChangePatch = (
stored: Record<string, unknown>,
formValues: Record<string, unknown>,
selection: CredentialSelection,
): CredentialValuesPatch => {
const desired = { ...typedFormValues(formValues), ...fixedValuesFor(selection) };
return {
credential_values: changedValues(stored, desired),
credential_values_to_delete: Object.keys(stored).filter((key) => !(key in desired)),
};
};
export const buildCredentialPatch = (
stored: Record<string, unknown>,
formValues: Record<string, unknown>,
initial: CredentialSelection,
selection: CredentialSelection,
): CredentialValuesPatch => {
const changed = changedValues(stored, { ...typedFormValues(formValues), ...fixedValuesFor(selection) });
const cleared = Object.keys(formValues).filter((key) => isBlank(formValues[key]) && !isBlank(stored[key]));
const toDelete = [...keysLeftBehind(initial, selection), ...cleared].filter(
(key) => key in stored && !(key in changed),
);
return { credential_values: changed, credential_values_to_delete: Array.from(new Set(toDelete)) };
};

View file

@ -0,0 +1,345 @@
import { describe, expect, it } from "vitest";
import {
buildCreateCredentialValues,
buildCredentialPatch,
buildProviderChangePatch,
federatedProviderOf,
inferAuthMethod,
isFederatedCredential,
providerFieldValidators,
selectionFor,
} from "./credential_federation";
const apiKeySelection = { authMethod: "api_key" } as const;
const tokenFileSelection = { authMethod: "federation", provider: "anthropic", identitySource: "token_file" } as const;
const internalIssuerSelection = {
authMethod: "federation",
provider: "anthropic",
identitySource: "internal_issuer",
} as const;
const keycloakSelection = { authMethod: "federation", provider: "anthropic", identitySource: "keycloak" } as const;
const environmentSelection = {
authMethod: "federation",
provider: "anthropic",
identitySource: "environment",
} as const;
const openAISelection = { authMethod: "federation", provider: "openai" } as const;
const storedTokenFile = {
api_base: "https://api.anthropic.com",
anthropic_federation_rule_id: "fdrl_stored",
anthropic_organization_id: "org-stored",
anthropic_federation_workspace_id: "wrkspc_stored",
anthropic_identity_token_file: "/var****",
};
const storedKeycloak = {
anthropic_federation_rule_id: "fdrl_stored",
anthropic_organization_id: "org-stored",
anthropic_identity_source: "keycloak",
anthropic_keycloak_token_url: "http****",
anthropic_keycloak_client_id: "lite****",
anthropic_keycloak_client_secret_ref: "os.e****",
anthropic_keycloak_auth_method: "clie****",
anthropic_keycloak_scope: "open****",
};
const storedOpenAI = {
api_base: "https://api.openai.com/v1",
openai_identity_provider_id: "idp_stored",
openai_service_account_id: "svc_stored",
openai_identity_token_file: "/var****",
};
describe("federatedProviderOf", () => {
it.each([
["Anthropic", "anthropic"],
["anthropic", "anthropic"],
["OpenAI", "openai"],
["openai", "openai"],
])("offers federation for %s", (provider, expected) => {
expect(federatedProviderOf(provider)).toBe(expected);
});
it.each(["OpenAI_Compatible", "OpenAI_Text", "Anthropic Text", "Azure", "", null, undefined])(
"offers no federation for %s",
(provider) => {
expect(federatedProviderOf(provider)).toBeNull();
},
);
});
describe("selectionFor", () => {
it("falls back to an api key for a provider without federation, whatever the auth method", () => {
expect(selectionFor(null, "federation", "keycloak")).toEqual(apiKeySelection);
expect(selectionFor("openai", "api_key", "keycloak")).toEqual(apiKeySelection);
});
it("carries the identity source for Anthropic only", () => {
expect(selectionFor("anthropic", "federation", "keycloak")).toEqual(keycloakSelection);
expect(selectionFor("openai", "federation", "keycloak")).toEqual(openAISelection);
});
});
describe("providerFieldValidators", () => {
it("checks the base URL of an OpenAI federated credential only", () => {
expect(providerFieldValidators(openAISelection).api_base?.("https://gateway.example.com/v1")).toEqual(
expect.stringContaining("OpenAI API"),
);
expect(providerFieldValidators(tokenFileSelection)).toEqual({});
expect(providerFieldValidators(apiKeySelection)).toEqual({});
});
});
describe("reading a stored credential", () => {
it("treats a credential with any federation value as federated", () => {
expect(isFederatedCredential({ anthropic_federation_rule_id: "fdrl_1" })).toBe(true);
expect(inferAuthMethod({ anthropic_identity_source: "keycloak" })).toBe("federation");
});
it("treats an api key credential, an empty one, and a missing one as not federated", () => {
expect(isFederatedCredential({ api_key: "sk-1****", api_base: "https://api.anthropic.com" })).toBe(false);
expect(isFederatedCredential({ anthropic_federation_rule_id: "" })).toBe(false);
expect(inferAuthMethod(undefined)).toBe("api_key");
});
it("opens a credential that stores an api key next to federation values as an api key credential", () => {
expect(inferAuthMethod({ api_key: "sk-1****", anthropic_federation_rule_id: "fdrl_1" })).toBe("api_key");
expect(inferAuthMethod({ api_key: "", anthropic_federation_rule_id: "fdrl_1" })).toBe("federation");
});
it("reads an OpenAI credential with federation values and no api key as federated", () => {
expect(inferAuthMethod({ openai_service_account_id: "svc_1" })).toBe("federation");
expect(inferAuthMethod({ api_key: "sk-p****", openai_service_account_id: "svc_1" })).toBe("api_key");
expect(isFederatedCredential({ api_base: "https://api.openai.com/v1", openai_identity_token_file: "" })).toBe(
false,
);
});
});
describe("buildCreateCredentialValues", () => {
it("sends the typed federation values and nothing for the fields left empty", () => {
const typedValues = {
api_base: "",
anthropic_federation_rule_id: " fdrl_new\n",
anthropic_organization_id: "org-new",
anthropic_service_account_id: "",
anthropic_federation_workspace_id: undefined,
anthropic_identity_token_file: "/var/run/secrets/anthropic/token",
};
expect(buildCreateCredentialValues(typedValues, tokenFileSelection)).toEqual({
anthropic_federation_rule_id: "fdrl_new",
anthropic_organization_id: "org-new",
anthropic_identity_token_file: "/var/run/secrets/anthropic/token",
});
});
it("names the identity source and sends the lifetime as a number for the internal issuer", () => {
const typedValues = {
anthropic_federation_rule_id: "fdrl_new",
anthropic_organization_id: "org-new",
anthropic_issuer_url: "https://litellm.example.com",
anthropic_issuer_subject: "litellm-proxy",
anthropic_issuer_ttl_seconds: "120",
anthropic_issuer_signing_key_ref: "os.environ/ISSUER_KEY",
};
const values = buildCreateCredentialValues(typedValues, internalIssuerSelection);
expect(values.anthropic_identity_source).toBe("internal_issuer");
expect(values.anthropic_issuer_ttl_seconds).toBe(120);
});
it("sends the trimmed OpenAI federation values without naming an Anthropic identity source", () => {
const typedValues = {
api_base: "https://api.openai.com/v1",
organization: "",
openai_identity_provider_id: "",
openai_service_account_id: " svc_new ",
openai_identity_token_file: "/var/run/secrets/kubernetes.io/serviceaccount/token\n",
};
expect(buildCreateCredentialValues(typedValues, openAISelection)).toEqual({
api_base: "https://api.openai.com/v1",
openai_service_account_id: "svc_new",
openai_identity_token_file: "/var/run/secrets/kubernetes.io/serviceaccount/token",
});
});
it("does not name an identity source for an api key credential and keeps its values as typed", () => {
expect(buildCreateCredentialValues({ api_key: " sk-ant-typed ", api_base: "" }, apiKeySelection)).toEqual({
api_key: " sk-ant-typed ",
});
});
});
describe("buildCredentialPatch", () => {
it("writes nothing when the admin saves a federated credential untouched", () => {
expect(
buildCredentialPatch(storedTokenFile, { ...storedTokenFile }, tokenFileSelection, tokenFileSelection),
).toEqual({ credential_values: {}, credential_values_to_delete: [] });
expect(buildCredentialPatch(storedKeycloak, { ...storedKeycloak }, keycloakSelection, keycloakSelection)).toEqual({
credential_values: {},
credential_values_to_delete: [],
});
});
it("writes nothing when the admin saves an OpenAI federated credential untouched", () => {
expect(buildCredentialPatch(storedOpenAI, { ...storedOpenAI }, openAISelection, openAISelection)).toEqual({
credential_values: {},
credential_values_to_delete: [],
});
});
it("deletes the OpenAI federation values and keeps the base URL when the admin switches to an api key", () => {
const patch = buildCredentialPatch(
storedOpenAI,
{ api_base: storedOpenAI.api_base, api_key: "sk-proj-new" },
openAISelection,
apiKeySelection,
);
expect(patch.credential_values).toEqual({ api_key: "sk-proj-new" });
expect([...patch.credential_values_to_delete].sort()).toEqual([
"openai_identity_provider_id",
"openai_identity_token_file",
"openai_service_account_id",
]);
});
it("sends only the value the admin changed", () => {
expect(
buildCredentialPatch(
storedTokenFile,
{ ...storedTokenFile, anthropic_organization_id: "org-edited" },
tokenFileSelection,
tokenFileSelection,
),
).toEqual({ credential_values: { anthropic_organization_id: "org-edited" }, credential_values_to_delete: [] });
});
it("replaces a hidden stored value when the admin types a new one", () => {
const patch = buildCredentialPatch(
storedTokenFile,
{ ...storedTokenFile, anthropic_identity_token_file: "/run/secrets/new-token" },
tokenFileSelection,
tokenFileSelection,
);
expect(patch.credential_values).toEqual({ anthropic_identity_token_file: "/run/secrets/new-token" });
});
it("deletes every stored value the admin cleared, a base URL included", () => {
expect(
buildCredentialPatch(
{ ...storedTokenFile, api_base: "https://gateway.example.com" },
{ ...storedTokenFile, api_base: "", anthropic_federation_workspace_id: " " },
tokenFileSelection,
tokenFileSelection,
),
).toEqual({
credential_values: {},
credential_values_to_delete: ["api_base", "anthropic_federation_workspace_id"],
});
});
it("deletes every stored value the admin did not re-enter when the provider changed", () => {
const stored = { api_base: "https://corp.openai.azure.com", api_version: "2024-10-21", api_key: "sk-1****" };
const typed = { anthropic_federation_rule_id: "fdrl_1", anthropic_identity_token_file: "/run/secrets/token" };
expect(buildProviderChangePatch(stored, typed, tokenFileSelection)).toEqual({
credential_values: typed,
credential_values_to_delete: ["api_base", "api_version", "api_key"],
});
});
it("does not treat an unchanged stored lifetime as an edit", () => {
const stored = {
...storedTokenFile,
anthropic_identity_source: "internal_issuer",
anthropic_issuer_ttl_seconds: 300,
};
const patch = buildCredentialPatch(
stored,
{ anthropic_issuer_ttl_seconds: "300" },
internalIssuerSelection,
internalIssuerSelection,
);
expect(patch.credential_values).toEqual({});
});
it("drops the other source's stored values when the admin switches identity source", () => {
const typedValues = {
anthropic_federation_rule_id: "fdrl_stored",
anthropic_organization_id: "org-stored",
anthropic_issuer_url: "https://litellm.example.com",
anthropic_issuer_subject: "litellm-proxy",
anthropic_issuer_signing_key_ref: "os.environ/ISSUER_KEY",
};
const patch = buildCredentialPatch(storedKeycloak, typedValues, keycloakSelection, internalIssuerSelection);
const expectedValues = {
anthropic_identity_source: "internal_issuer",
anthropic_issuer_url: "https://litellm.example.com",
anthropic_issuer_subject: "litellm-proxy",
anthropic_issuer_signing_key_ref: "os.environ/ISSUER_KEY",
};
expect(patch.credential_values).toEqual(expectedValues);
expect([...patch.credential_values_to_delete].sort()).toEqual([
"anthropic_keycloak_auth_method",
"anthropic_keycloak_client_id",
"anthropic_keycloak_client_secret_ref",
"anthropic_keycloak_scope",
"anthropic_keycloak_token_url",
]);
});
it("drops the declared source when the admin switches to a token file or the proxy environment", () => {
const toEnvironment = buildCredentialPatch(storedKeycloak, {}, keycloakSelection, environmentSelection);
expect(toEnvironment.credential_values).toEqual({});
expect(toEnvironment.credential_values_to_delete).toContain("anthropic_identity_source");
expect(toEnvironment.credential_values_to_delete).not.toContain("anthropic_federation_rule_id");
});
it("deletes the stored api key when the admin switches the credential to federation", () => {
const typedValues = {
api_base: "https://api.anthropic.com",
anthropic_federation_rule_id: "fdrl_new",
anthropic_organization_id: "org-new",
anthropic_identity_token_file: "/var/run/secrets/anthropic/token",
};
const patch = buildCredentialPatch(
{ api_key: "sk-a****", api_base: "https://api.anthropic.com" },
typedValues,
apiKeySelection,
tokenFileSelection,
);
expect(patch.credential_values_to_delete).toEqual(["api_key"]);
expect(patch.credential_values).toEqual({
anthropic_federation_rule_id: "fdrl_new",
anthropic_organization_id: "org-new",
anthropic_identity_token_file: "/var/run/secrets/anthropic/token",
});
});
it("deletes every stored federation value when the admin switches the credential to an api key", () => {
const patch = buildCredentialPatch(storedKeycloak, { api_key: "sk-ant-new" }, keycloakSelection, apiKeySelection);
expect(patch.credential_values).toEqual({ api_key: "sk-ant-new" });
expect([...patch.credential_values_to_delete].sort()).toEqual(Object.keys(storedKeycloak).sort());
});
it("leaves a stored api key alone when the admin keeps a federated credential federated", () => {
const stored = { ...storedTokenFile, api_key: "sk-a****" };
const patch = buildCredentialPatch(
stored,
{ ...storedTokenFile, anthropic_organization_id: "org-edited" },
tokenFileSelection,
tokenFileSelection,
);
expect(patch.credential_values_to_delete).toEqual([]);
});
it("never names one key as both a write and a delete", () => {
const patch = buildCredentialPatch(
storedKeycloak,
{ ...storedKeycloak, anthropic_identity_token_file: "/var/run/secrets/anthropic/token" },
keycloakSelection,
tokenFileSelection,
);
const written = Object.keys(patch.credential_values);
expect(patch.credential_values_to_delete.filter((key) => written.includes(key))).toEqual([]);
});
});

View file

@ -0,0 +1,145 @@
import { isMaskedSecret } from "@/utils/maskedSecretUtils";
import {
ANTHROPIC_FEDERATION_FIELDS,
ANTHROPIC_FEDERATION_VALUE_KEYS,
identitySourceById,
isAnthropicProvider,
otherIdentitySourceKeys,
type IdentitySourceId,
} from "./anthropic_federation";
import { isBlank, type FederationField } from "./federation_field";
import { isOpenAIProvider, OPENAI_FEDERATION_FIELDS, validateOpenAIFederationApiBase } from "./openai_federation";
export type AuthMethod = "api_key" | "federation";
export type FederatedProvider = "anthropic" | "openai";
export type FederatedSelection =
| { readonly authMethod: "federation"; readonly provider: "openai" }
| { readonly authMethod: "federation"; readonly provider: "anthropic"; readonly identitySource: IdentitySourceId };
export type CredentialSelection = { readonly authMethod: "api_key" } | FederatedSelection;
export type ProviderFieldValidators = Readonly<Record<string, (value: unknown) => string | true>>;
export interface CredentialValuesPatch {
readonly credential_values: Record<string, unknown>;
readonly credential_values_to_delete: readonly string[];
}
const API_KEY = "api_key";
const FEDERATION_VALUE_KEYS: readonly string[] = [
...ANTHROPIC_FEDERATION_VALUE_KEYS,
...OPENAI_FEDERATION_FIELDS.map((field) => field.key),
];
const federationFieldsByKey: ReadonlyMap<string, FederationField> = new Map(
[...ANTHROPIC_FEDERATION_FIELDS, ...OPENAI_FEDERATION_FIELDS].map((field) => [field.key, field]),
);
const OPENAI_FEDERATION_PROVIDER_FIELD_VALIDATORS: ProviderFieldValidators = {
api_base: validateOpenAIFederationApiBase,
};
const NO_PROVIDER_FIELD_VALIDATORS: ProviderFieldValidators = {};
export const federatedProviderOf = (provider: string | null | undefined): FederatedProvider | null => {
if (isAnthropicProvider(provider)) {
return "anthropic";
}
return isOpenAIProvider(provider) ? "openai" : null;
};
export const selectionFor = (
provider: FederatedProvider | null,
authMethod: AuthMethod,
identitySource: IdentitySourceId,
): CredentialSelection => {
if (provider === null || authMethod === "api_key") {
return { authMethod: "api_key" };
}
return provider === "anthropic"
? { authMethod: "federation", provider, identitySource }
: { authMethod: "federation", provider };
};
export const isFederatedCredential = (credentialValues: Record<string, unknown> | null | undefined): boolean =>
FEDERATION_VALUE_KEYS.some((key) => !isBlank(credentialValues?.[key]));
export const inferAuthMethod = (credentialValues: Record<string, unknown> | null | undefined): AuthMethod =>
isBlank(credentialValues?.[API_KEY]) && isFederatedCredential(credentialValues) ? "federation" : "api_key";
export const providerFieldValidators = (selection: CredentialSelection): ProviderFieldValidators =>
selection.authMethod === "federation" && selection.provider === "openai"
? OPENAI_FEDERATION_PROVIDER_FIELD_VALIDATORS
: NO_PROVIDER_FIELD_VALIDATORS;
const identitySourceOf = (selection: CredentialSelection): IdentitySourceId | null =>
selection.authMethod === "federation" && selection.provider === "anthropic" ? selection.identitySource : null;
const toStoredType = (field: FederationField | undefined, value: unknown): unknown => {
if (field === undefined || isBlank(value)) {
return field === undefined ? value : "";
}
if (field.control === "integer") {
return Number(value);
}
return typeof value === "string" ? value.trim() : value;
};
const fixedValuesFor = (selection: CredentialSelection): Readonly<Record<string, string>> => {
const identitySource = identitySourceOf(selection);
return identitySource === null ? {} : identitySourceById(identitySource).fixedValues;
};
const typedFormValues = (formValues: Record<string, unknown>): Record<string, unknown> =>
Object.fromEntries(
Object.entries(formValues)
.map(([key, value]) => [key, toStoredType(federationFieldsByKey.get(key), value)] as const)
.filter(([, value]) => value !== "" && value !== undefined && value !== null),
);
export const buildCreateCredentialValues = (
formValues: Record<string, unknown>,
selection: CredentialSelection,
): Record<string, unknown> => ({ ...typedFormValues(formValues), ...fixedValuesFor(selection) });
const keysLeftBehind = (initial: CredentialSelection, selection: CredentialSelection): readonly string[] => {
if (selection.authMethod !== initial.authMethod) {
return selection.authMethod === "federation" ? [API_KEY] : FEDERATION_VALUE_KEYS;
}
const identitySource = identitySourceOf(selection);
return identitySource === null || identitySource === identitySourceOf(initial)
? []
: otherIdentitySourceKeys(identitySource);
};
const changedValues = (stored: Record<string, unknown>, desired: Record<string, unknown>): Record<string, unknown> =>
Object.fromEntries(Object.entries(desired).filter(([key, value]) => !isMaskedSecret(value) && value !== stored[key]));
export const buildProviderChangePatch = (
stored: Record<string, unknown>,
formValues: Record<string, unknown>,
selection: CredentialSelection,
): CredentialValuesPatch => {
const desired = { ...typedFormValues(formValues), ...fixedValuesFor(selection) };
return {
credential_values: changedValues(stored, desired),
credential_values_to_delete: Object.keys(stored).filter((key) => !(key in desired)),
};
};
export const buildCredentialPatch = (
stored: Record<string, unknown>,
formValues: Record<string, unknown>,
initial: CredentialSelection,
selection: CredentialSelection,
): CredentialValuesPatch => {
const changed = changedValues(stored, { ...typedFormValues(formValues), ...fixedValuesFor(selection) });
const cleared = Object.keys(formValues).filter((key) => isBlank(formValues[key]) && !isBlank(stored[key]));
const toDelete = [...keysLeftBehind(initial, selection), ...cleared].filter(
(key) => key in stored && !(key in changed),
);
return { credential_values: changed, credential_values_to_delete: Array.from(new Set(toDelete)) };
};

View file

@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { requiredFederationValue, validateMaskedValueUntouched } from "./federation_field";
describe("federation field validation", () => {
it("refuses a hidden stored value that was only partly edited", () => {
const rule = validateMaskedValueUntouched("os.e****");
expect(rule("os.e****")).toBe(true);
expect(rule("os.environ/NEW_REF")).toBe(true);
expect(rule("os.e****_NEW")).toEqual(expect.stringContaining("Replace the whole value"));
});
it.each(["", " ", "\n", undefined, null])("refuses the blank required value %j", (value) => {
expect(requiredFederationValue(value)).toBe("Required");
});
it("accepts a required value with text in it", () => {
expect(requiredFederationValue(" svc_1 ")).toBe(true);
});
});

View file

@ -0,0 +1,27 @@
import { isMaskedSecret } from "@/utils/maskedSecretUtils";
export interface FederationField {
readonly key: string;
readonly label: string;
readonly tooltip: string;
readonly placeholder?: string;
readonly required: boolean;
readonly control: "text" | "integer" | "select";
readonly options?: readonly string[];
}
export const isBlank = (value: unknown): boolean => {
if (typeof value === "string") {
return value.trim() === "";
}
return value === undefined || value === null;
};
export const requiredFederationValue = (value: unknown): string | true => (isBlank(value) ? "Required" : true);
export const validateMaskedValueUntouched =
(storedValue: unknown) =>
(value: unknown): string | true =>
isMaskedSecret(value) && value !== storedValue
? "This stored value is hidden. Replace the whole value to change it"
: true;

View file

@ -0,0 +1,38 @@
import { describe, expect, it } from "vitest";
import { isOpenAIProvider, validateOpenAIFederationApiBase } from "./openai_federation";
describe("isOpenAIProvider", () => {
it.each(["OpenAI", "openai"])("matches %s", (provider) => {
expect(isOpenAIProvider(provider)).toBe(true);
});
it.each(["OpenAI_Compatible", "OpenAI_Text", "Azure", "", null, undefined])("does not match %s", (provider) => {
expect(isOpenAIProvider(provider)).toBe(false);
});
});
describe("validateOpenAIFederationApiBase", () => {
it.each([
"",
" ",
undefined,
"https://api.openai.com/v1",
"https://api.openai.com",
"https://us.api.openai.com/v1",
"https://API.OPENAI.COM/v1",
])("accepts %s, which the proxy sends the federated token to", (apiBase) => {
expect(validateOpenAIFederationApiBase(apiBase)).toBe(true);
});
it.each([
"http://api.openai.com/v1",
"https://api.openai.com.evil.example/v1",
"https://evilapi.openai.com/v1",
"https://openai.com/v1",
"https://gateway.example.com/v1",
"api.openai.com/v1",
"not a url",
])("refuses %s, which the proxy rejects for workload identity federation", (apiBase) => {
expect(validateOpenAIFederationApiBase(apiBase)).toEqual(expect.stringContaining("only reaches the OpenAI API"));
});
});

View file

@ -0,0 +1,46 @@
import { isBlank, type FederationField } from "./federation_field";
const OPENAI_API_HOST = "api.openai.com";
export const OPENAI_FEDERATION_FIELDS: readonly FederationField[] = [
{
key: "openai_identity_provider_id",
label: "Identity Provider ID",
tooltip:
"The workload identity provider registered in your OpenAI organization that trusts the token's issuer. Leave empty when the proxy sets OPENAI_IDENTITY_PROVIDER_ID in its environment.",
required: false,
control: "text",
},
{
key: "openai_service_account_id",
label: "Service Account ID",
tooltip:
"The OpenAI service account the verified identity acts as. Its project decides what the model can reach. Leave empty when the proxy sets OPENAI_SERVICE_ACCOUNT_ID in its environment.",
required: false,
control: "text",
},
{
key: "openai_identity_token_file",
label: "Identity Token File",
tooltip:
"Absolute path on the proxy host of a file holding the identity token, for example a projected Kubernetes service account token. Leave empty when the proxy sets OPENAI_IDENTITY_TOKEN_FILE in its environment.",
placeholder: "/var/run/secrets/kubernetes.io/serviceaccount/token",
required: false,
control: "text",
},
];
export const isOpenAIProvider = (provider: string | null | undefined): boolean => provider?.toLowerCase() === "openai";
const isOpenAIApiHost = (hostname: string): boolean =>
hostname === OPENAI_API_HOST || hostname.endsWith(`.${OPENAI_API_HOST}`);
const targetsOpenAIApi = (apiBase: string): boolean => {
const url = URL.canParse(apiBase) ? new URL(apiBase) : null;
return url?.protocol === "https:" && isOpenAIApiHost(url.hostname);
};
export const validateOpenAIFederationApiBase = (value: unknown): string | true =>
typeof value !== "string" || isBlank(value) || targetsOpenAIApi(value)
? true
: `Workload identity federation only reaches the OpenAI API. Enter https://${OPENAI_API_HOST}/v1 or a regional *.${OPENAI_API_HOST} URL, or leave this empty`;