Merge branch 'aig' of github.com:fosrl/pangolin into aig

This commit is contained in:
Owen
2026-08-11 15:58:12 -04:00
4 changed files with 318 additions and 216 deletions
+4
View File
@@ -855,12 +855,16 @@
"authMethodsSave": "Save Settings", "authMethodsSave": "Save Settings",
"policyAuthStackTitle": "Authentication", "policyAuthStackTitle": "Authentication",
"policyAuthStackDescription": "Control which authentication methods are required to access this resource", "policyAuthStackDescription": "Control which authentication methods are required to access this resource",
"policyAuthInferenceStackDescription": "Choose which users and roles can authenticate to this AI gateway",
"policyAuthOrLogicTitle": "Multiple authentication methods active", "policyAuthOrLogicTitle": "Multiple authentication methods active",
"policyAuthOrLogicBanner": "Visitors may authenticate using any one of the active methods below. They do not need to complete all of them.", "policyAuthOrLogicBanner": "Visitors may authenticate using any one of the active methods below. They do not need to complete all of them.",
"policyAuthMethodActive": "Active", "policyAuthMethodActive": "Active",
"policyAuthMethodOff": "Off", "policyAuthMethodOff": "Off",
"policyAuthSsoTitle": "Platform SSO", "policyAuthSsoTitle": "Platform SSO",
"policyAuthSsoDescription": "Require sign-in through your organization's identity provider", "policyAuthSsoDescription": "Require sign-in through your organization's identity provider",
"policyAuthInferenceSsoDescription": "Selected users and roles can authenticate to the gateway using their identity API key",
"policyAuthInferenceIdentityKeyHelp": "Every user already has an identity API key, so you only need to create virtual API keys for non-user clients or shared access. Users can retrieve their key by signing in with their identity provider at <resourceLink></resourceLink>, where it will be shown after login.",
"policyAuthInferenceIdentityKeyHelpNoUrl": "Every user already has an identity API key, so you only need to create virtual API keys for non-user clients or shared access. Users can retrieve their key by signing in with their identity provider at this resource's URL, where it will be shown after login.",
"policyAuthSsoSummary": "{idp} · {users} users, {roles} roles", "policyAuthSsoSummary": "{idp} · {users} users, {roles} roles",
"policyAuthSsoDefaultIdp": "Default provider", "policyAuthSsoDefaultIdp": "Default provider",
"policyAuthAddDefaultIdentityProvider": "Add Default Identity Provider", "policyAuthAddDefaultIdentityProvider": "Add Default Identity Provider",
-24
View File
@@ -372,30 +372,6 @@ export default function VirtualApiKeysTable({
}, },
cell: ({ row }) => moment(row.original.createdAt).format("lll") cell: ({ row }) => moment(row.original.createdAt).format("lll")
}, },
{
accessorKey: "expiresAt",
friendlyName: t("expires"),
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t("expires")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
cell: ({ row }) => {
const expiresAt = row.original.expiresAt;
if (expiresAt) {
return moment(expiresAt).format("lll");
}
return t("never");
}
},
{ {
id: "actions", id: "actions",
enableHiding: false, enableHiding: false,
@@ -29,6 +29,9 @@ export type PolicyAuthSsoSectionProps = {
usersEditor: React.ReactNode; usersEditor: React.ReactNode;
disabled?: boolean; disabled?: boolean;
idpDisabled?: boolean; idpDisabled?: boolean;
ssoLocked?: boolean;
title?: string;
description?: string;
}; };
export function PolicyAuthSsoSection({ export function PolicyAuthSsoSection({
@@ -40,7 +43,10 @@ export function PolicyAuthSsoSection({
rolesEditor, rolesEditor,
usersEditor, usersEditor,
disabled, disabled,
idpDisabled idpDisabled,
ssoLocked,
title,
description
}: PolicyAuthSsoSectionProps) { }: PolicyAuthSsoSectionProps) {
const t = useTranslations(); const t = useTranslations();
const [showIdpSelect, setShowIdpSelect] = useState(skipToIdpId != null); const [showIdpSelect, setShowIdpSelect] = useState(skipToIdpId != null);
@@ -52,22 +58,34 @@ export function PolicyAuthSsoSection({
}, [skipToIdpId]); }, [skipToIdpId]);
const idpSelectDisabled = idpDisabled ?? disabled; const idpSelectDisabled = idpDisabled ?? disabled;
const ssoActive = ssoLocked || sso;
const ssoTitle = title ?? t("policyAuthSsoTitle");
const ssoDescription = description ?? t("policyAuthSsoDescription");
return ( return (
<SettingsFormGrid> <SettingsFormGrid>
<SettingsFormCell span="full"> {!ssoLocked && (
<SwitchInput <SettingsFormCell span="full">
id="policy-auth-sso" <SwitchInput
label={t("policyAuthSsoTitle")} id="policy-auth-sso"
description={t("policyAuthSsoDescription")} label={ssoTitle}
checked={sso} description={ssoDescription}
disabled={disabled} checked={ssoActive}
onCheckedChange={onSsoChange} disabled={disabled}
/> onCheckedChange={onSsoChange}
</SettingsFormCell> />
</SettingsFormCell>
)}
{sso && ( {ssoActive && (
<> <>
{ssoLocked && ssoDescription && (
<SettingsFormCell span="full">
<p className="text-sm text-muted-foreground">
{ssoDescription}
</p>
</SettingsFormCell>
)}
<SettingsFormCell span="full"> <SettingsFormCell span="full">
<FormItem> <FormItem>
<FormLabel>{t("roles")}</FormLabel> <FormLabel>{t("roles")}</FormLabel>
@@ -15,6 +15,7 @@ import {
} from "@app/components/roles-selector"; } from "@app/components/roles-selector";
import { UsersSelector } from "@app/components/users-selector"; import { UsersSelector } from "@app/components/users-selector";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import { Form, FormField } from "@app/components/ui/form"; import { Form, FormField } from "@app/components/ui/form";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
@@ -28,8 +29,10 @@ import type { GetResourcePolicyResponse } from "@server/routers/policy";
import { UserType } from "@server/types/UserTypes"; import { UserType } from "@server/types/UserTypes";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import type { AxiosResponse } from "axios"; import type { AxiosResponse } from "axios";
import { ExternalLink, InfoIcon } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { toUnicode } from "punycode";
import { import {
useActionState, useActionState,
useContext, useContext,
@@ -108,9 +111,21 @@ export function PolicyAuthStackSectionEdit({
const resourceContext = useContext(ResourceContext); const resourceContext = useContext(ResourceContext);
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const isInferenceResource = resourceContext?.resource.mode === "inference";
const isResourceOverlay = resourceId !== undefined; const isResourceOverlay = resourceId !== undefined;
const authReadonly = readonly || isResourceOverlay; const authReadonly = readonly || isResourceOverlay;
const inferenceResourceUrl = useMemo(() => {
if (!isInferenceResource || !resourceContext?.resource) {
return null;
}
const { ssl, fullDomain } = resourceContext.resource;
if (!fullDomain) {
return null;
}
return `${ssl ? "https" : "http"}://${toUnicode(fullDomain)}`;
}, [isInferenceResource, resourceContext?.resource]);
const policyRoleItems = useMemo<OverlaySelectedRole[]>( const policyRoleItems = useMemo<OverlaySelectedRole[]>(
() => () =>
policy.roles.map((r) => ({ policy.roles.map((r) => ({
@@ -264,6 +279,12 @@ export function PolicyAuthStackSectionEdit({
const overlayRoles = combinedRoles.filter((r) => !r.isAdmin); const overlayRoles = combinedRoles.filter((r) => !r.isAdmin);
const overlayUsers = combinedUsers; const overlayUsers = combinedUsers;
useEffect(() => {
if (isInferenceResource && !form.getValues("sso")) {
form.setValue("sso", true);
}
}, [isInferenceResource, form]);
const [, formAction, isSubmitting] = useActionState(onSubmit, null); const [, formAction, isSubmitting] = useActionState(onSubmit, null);
const [isSavingOverlay, setIsSavingOverlay] = useState(false); const [isSavingOverlay, setIsSavingOverlay] = useState(false);
@@ -294,7 +315,7 @@ export function PolicyAuthStackSectionEdit({
.put( .put(
`/resource-policy/${policy.resourcePolicyId}/access-control`, `/resource-policy/${policy.resourcePolicyId}/access-control`,
{ {
sso: payload.sso, sso: isInferenceResource ? true : payload.sso,
userIds: payload.users.map((user) => user.id), userIds: payload.users.map((user) => user.id),
roleIds: payload.roles.map((role) => Number(role.id)), roleIds: payload.roles.map((role) => Number(role.id)),
skipToIdpId: payload.skipToIdpId skipToIdpId: payload.skipToIdpId
@@ -302,94 +323,101 @@ export function PolicyAuthStackSectionEdit({
) )
.catch(handleError) .catch(handleError)
); );
policyUpdates.sso = isInferenceResource ? true : payload.sso;
if (!isInferenceResource) {
if (passcodeActive && payload.password?.password) {
requests.push(
api
.put(
`/resource-policy/${policy.resourcePolicyId}/password`,
{ password: payload.password.password }
)
.catch(handleError)
);
policyUpdates.passwordId = policy.passwordId ?? -1;
} else if (!passcodeActive && passcodeOnServerRef.current) {
requests.push(
api
.put(
`/resource-policy/${policy.resourcePolicyId}/password`,
{ password: null }
)
.catch(handleError)
);
policyUpdates.passwordId = null;
}
if (pinActive && payload.pincode?.pincode?.length === 6) {
requests.push(
api
.put(
`/resource-policy/${policy.resourcePolicyId}/pincode`,
{ pincode: payload.pincode.pincode }
)
.catch(handleError)
);
policyUpdates.pincodeId = policy.pincodeId ?? -1;
} else if (!pinActive && pincodeOnServerRef.current) {
requests.push(
api
.put(
`/resource-policy/${policy.resourcePolicyId}/pincode`,
{ pincode: null }
)
.catch(handleError)
);
policyUpdates.pincodeId = null;
}
if (
headerAuthActive &&
payload.headerAuth?.user &&
payload.headerAuth?.password
) {
requests.push(
api
.put(
`/resource-policy/${policy.resourcePolicyId}/header-auth`,
{ headerAuth: payload.headerAuth }
)
.catch(handleError)
);
policyUpdates.headerAuth = {
id: policy.headerAuth?.id ?? -1,
extendedCompability:
payload.headerAuth.extendedCompatibility ?? true
};
} else if (!headerAuthActive && headerAuthOnServerRef.current) {
requests.push(
api
.put(
`/resource-policy/${policy.resourcePolicyId}/header-auth`,
{ headerAuth: null }
)
.catch(handleError)
);
policyUpdates.headerAuth = {
id: null,
extendedCompability: null
} as unknown as GetResourcePolicyResponse["headerAuth"];
}
if (passcodeActive && payload.password?.password) {
requests.push( requests.push(
api api
.put( .put(
`/resource-policy/${policy.resourcePolicyId}/password`, `/resource-policy/${policy.resourcePolicyId}/whitelist`,
{ password: payload.password.password } {
emailWhitelistEnabled:
payload.emailWhitelistEnabled,
emails: payload.emails?.map((e) => e.text) ?? []
}
) )
.catch(handleError) .catch(handleError)
); );
policyUpdates.passwordId = policy.passwordId ?? -1; policyUpdates.emailWhitelistEnabled = payload.emailWhitelistEnabled;
} else if (!passcodeActive && passcodeOnServerRef.current) {
requests.push(
api
.put(
`/resource-policy/${policy.resourcePolicyId}/password`,
{ password: null }
)
.catch(handleError)
);
policyUpdates.passwordId = null;
} }
if (pinActive && payload.pincode?.pincode?.length === 6) {
requests.push(
api
.put(
`/resource-policy/${policy.resourcePolicyId}/pincode`,
{ pincode: payload.pincode.pincode }
)
.catch(handleError)
);
policyUpdates.pincodeId = policy.pincodeId ?? -1;
} else if (!pinActive && pincodeOnServerRef.current) {
requests.push(
api
.put(
`/resource-policy/${policy.resourcePolicyId}/pincode`,
{ pincode: null }
)
.catch(handleError)
);
policyUpdates.pincodeId = null;
}
if (
headerAuthActive &&
payload.headerAuth?.user &&
payload.headerAuth?.password
) {
requests.push(
api
.put(
`/resource-policy/${policy.resourcePolicyId}/header-auth`,
{ headerAuth: payload.headerAuth }
)
.catch(handleError)
);
policyUpdates.headerAuth = {
id: policy.headerAuth?.id ?? -1,
extendedCompability:
payload.headerAuth.extendedCompatibility ?? true
};
} else if (!headerAuthActive && headerAuthOnServerRef.current) {
requests.push(
api
.put(
`/resource-policy/${policy.resourcePolicyId}/header-auth`,
{ headerAuth: null }
)
.catch(handleError)
);
policyUpdates.headerAuth = {
id: null,
extendedCompability: null
} as unknown as GetResourcePolicyResponse["headerAuth"];
}
requests.push(
api
.put(`/resource-policy/${policy.resourcePolicyId}/whitelist`, {
emailWhitelistEnabled: payload.emailWhitelistEnabled,
emails: payload.emails?.map((e) => e.text) ?? []
})
.catch(handleError)
);
policyUpdates.emailWhitelistEnabled = payload.emailWhitelistEnabled;
try { try {
const results = await Promise.all(requests); const results = await Promise.all(requests);
if (results.every((res) => res && res.status === 200)) { if (results.every((res) => res && res.status === 200)) {
@@ -411,13 +439,17 @@ export function PolicyAuthStackSectionEdit({
updatePolicy(policyUpdates); updatePolicy(policyUpdates);
resourceContext?.updateAuthInfo({ resourceContext?.updateAuthInfo(
sso: payload.sso, isInferenceResource
whitelist: payload.emailWhitelistEnabled, ? { sso: true }
password: passcodeOnServerRef.current, : {
pincode: pincodeOnServerRef.current, sso: payload.sso,
headerAuth: headerAuthOnServerRef.current whitelist: payload.emailWhitelistEnabled,
}); password: passcodeOnServerRef.current,
pincode: pincodeOnServerRef.current,
headerAuth: headerAuthOnServerRef.current
}
);
toast({ toast({
title: t("success"), title: t("success"),
@@ -514,19 +546,53 @@ export function PolicyAuthStackSectionEdit({
{t("policyAuthStackTitle")} {t("policyAuthStackTitle")}
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t("policyAuthStackDescription")} {isInferenceResource
? t("policyAuthInferenceStackDescription")
: t("policyAuthStackDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
{isResourceOverlay && ( {isResourceOverlay && (
<SharedPolicyResourceNotice section="authentication" /> <SharedPolicyResourceNotice section="authentication" />
)} )}
{isInferenceResource && (
<Alert variant="neutral">
<InfoIcon className="h-4 w-4" />
<AlertDescription>
{inferenceResourceUrl
? t.rich(
"policyAuthInferenceIdentityKeyHelp",
{
resourceLink: () => (
<a
href={
inferenceResourceUrl
}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
{inferenceResourceUrl}
<ExternalLink className="ml-1 inline size-3.5 shrink-0 align-text-bottom" />
</a>
)
}
)
: t(
"policyAuthInferenceIdentityKeyHelpNoUrl"
)}
</AlertDescription>
</Alert>
)}
<SettingsSectionForm variant="half"> <SettingsSectionForm variant="half">
<PolicyAuthSsoSection <PolicyAuthSsoSection
sso={Boolean(sso)} sso={Boolean(sso) || isInferenceResource}
onSsoChange={(active) => onSsoChange={(active) => {
form.setValue("sso", active) if (isInferenceResource) {
} return;
}
form.setValue("sso", active);
}}
skipToIdpId={skipToIdpId} skipToIdpId={skipToIdpId}
onSkipToIdpChange={(id) => onSkipToIdpChange={(id) =>
form.setValue("skipToIdpId", id) form.setValue("skipToIdpId", id)
@@ -534,6 +600,12 @@ export function PolicyAuthStackSectionEdit({
allIdps={allIdps} allIdps={allIdps}
disabled={authReadonly} disabled={authReadonly}
idpDisabled={authReadonly} idpDisabled={authReadonly}
ssoLocked={isInferenceResource}
description={
isInferenceResource
? t("policyAuthInferenceSsoDescription")
: undefined
}
rolesEditor={ rolesEditor={
isResourceOverlay ? ( isResourceOverlay ? (
<RolesSelector <RolesSelector
@@ -605,100 +677,132 @@ export function PolicyAuthStackSectionEdit({
} }
/> />
<PolicyAuthOtherMethodsSection {!isInferenceResource && (
pinActive={pinActive} <PolicyAuthOtherMethodsSection
passcodeActive={passcodeActive} pinActive={pinActive}
emailWhitelistEnabled={Boolean( passcodeActive={passcodeActive}
emailWhitelistEnabled emailWhitelistEnabled={Boolean(
)} emailWhitelistEnabled
headerAuthActive={headerAuthActive} )}
headerAuthUser={headerAuth?.user ?? ""} headerAuthActive={headerAuthActive}
emailCount={emails.length} headerAuthUser={headerAuth?.user ?? ""}
emailEnabled={emailEnabled} emailCount={emails.length}
disabled={authReadonly} emailEnabled={emailEnabled}
onConfigure={openMethodEditor} disabled={authReadonly}
onTogglePincode={(active) => onConfigure={openMethodEditor}
handleToggle("pincode", active, () => { onTogglePincode={(active) =>
setPinActive(false); handleToggle("pincode", active, () => {
form.setValue("pincode", null); setPinActive(false);
}) form.setValue("pincode", null);
} })
onTogglePasscode={(active) => }
handleToggle("passcode", active, () => { onTogglePasscode={(active) =>
setPasscodeActive(false); handleToggle("passcode", active, () => {
form.setValue("password", null); setPasscodeActive(false);
}) form.setValue("password", null);
} })
onToggleEmail={(active) => }
handleToggle("email", active, () => onToggleEmail={(active) =>
form.setValue( handleToggle("email", active, () =>
"emailWhitelistEnabled", form.setValue(
false "emailWhitelistEnabled",
false
)
) )
) }
} onToggleHeaderAuth={(active) =>
onToggleHeaderAuth={(active) => handleToggle(
handleToggle("headerAuth", active, () => { "headerAuth",
setHeaderAuthActive(false); active,
form.setValue("headerAuth", null); () => {
}) setHeaderAuthActive(false);
} form.setValue(
/> "headerAuth",
null
);
}
)
}
/>
)}
</SettingsSectionForm> </SettingsSectionForm>
<PincodeCredenza {!isInferenceResource && (
open={editingMethod === "pincode"} <>
onOpenChange={(open) => !open && closeCredenza()} <PincodeCredenza
defaultPincode={pincode?.pincode ?? ""} open={editingMethod === "pincode"}
onSave={(value) => { onOpenChange={(open) =>
form.setValue("pincode", { pincode: value }); !open && closeCredenza()
setPinActive(true); }
}} defaultPincode={pincode?.pincode ?? ""}
/> onSave={(value) => {
form.setValue("pincode", {
pincode: value
});
setPinActive(true);
}}
/>
<PasscodeCredenza <PasscodeCredenza
open={editingMethod === "passcode"} open={editingMethod === "passcode"}
onOpenChange={(open) => !open && closeCredenza()} onOpenChange={(open) =>
defaultPassword={password?.password ?? ""} !open && closeCredenza()
existingConfigured={Boolean(policy.passwordId)} }
onSave={(value) => { defaultPassword={password?.password ?? ""}
form.setValue("password", { password: value }); existingConfigured={Boolean(
setPasscodeActive(true); policy.passwordId
}} )}
/> onSave={(value) => {
form.setValue("password", {
password: value
});
setPasscodeActive(true);
}}
/>
<EmailCredenza <EmailCredenza
open={editingMethod === "email"} open={editingMethod === "email"}
onOpenChange={(open) => !open && closeCredenza()} onOpenChange={(open) =>
emailEnabled={emailEnabled} !open && closeCredenza()
disabled={authReadonly} }
emails={emails} emailEnabled={emailEnabled}
onSave={(value) => { disabled={authReadonly}
form.setValue("emails", value); emails={emails}
form.setValue("emailWhitelistEnabled", true); onSave={(value) => {
}} form.setValue("emails", value);
/> form.setValue(
"emailWhitelistEnabled",
true
);
}}
/>
<HeaderAuthCredenza <HeaderAuthCredenza
open={editingMethod === "headerAuth"} open={editingMethod === "headerAuth"}
onOpenChange={(open) => !open && closeCredenza()} onOpenChange={(open) =>
defaultValues={ !open && closeCredenza()
headerAuth }
? { defaultValues={
user: headerAuth.user, headerAuth
password: headerAuth.password, ? {
extendedCompatibility: user: headerAuth.user,
headerAuth.extendedCompatibility ?? password: headerAuth.password,
true extendedCompatibility:
} headerAuth.extendedCompatibility ??
: undefined true
} }
existingConfigured={Boolean(policy.headerAuth?.id)} : undefined
onSave={(value) => { }
form.setValue("headerAuth", value); existingConfigured={Boolean(
setHeaderAuthActive(true); policy.headerAuth?.id
}} )}
/> onSave={(value) => {
form.setValue("headerAuth", value);
setHeaderAuthActive(true);
}}
/>
</>
)}
</SettingsSectionBody> </SettingsSectionBody>
<SettingsSectionFooter> <SettingsSectionFooter>
<Button <Button