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",
"policyAuthStackTitle": "Authentication",
"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",
"policyAuthOrLogicBanner": "Visitors may authenticate using any one of the active methods below. They do not need to complete all of them.",
"policyAuthMethodActive": "Active",
"policyAuthMethodOff": "Off",
"policyAuthSsoTitle": "Platform SSO",
"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",
"policyAuthSsoDefaultIdp": "Default 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")
},
{
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",
enableHiding: false,
@@ -29,6 +29,9 @@ export type PolicyAuthSsoSectionProps = {
usersEditor: React.ReactNode;
disabled?: boolean;
idpDisabled?: boolean;
ssoLocked?: boolean;
title?: string;
description?: string;
};
export function PolicyAuthSsoSection({
@@ -40,7 +43,10 @@ export function PolicyAuthSsoSection({
rolesEditor,
usersEditor,
disabled,
idpDisabled
idpDisabled,
ssoLocked,
title,
description
}: PolicyAuthSsoSectionProps) {
const t = useTranslations();
const [showIdpSelect, setShowIdpSelect] = useState(skipToIdpId != null);
@@ -52,22 +58,34 @@ export function PolicyAuthSsoSection({
}, [skipToIdpId]);
const idpSelectDisabled = idpDisabled ?? disabled;
const ssoActive = ssoLocked || sso;
const ssoTitle = title ?? t("policyAuthSsoTitle");
const ssoDescription = description ?? t("policyAuthSsoDescription");
return (
<SettingsFormGrid>
<SettingsFormCell span="full">
<SwitchInput
id="policy-auth-sso"
label={t("policyAuthSsoTitle")}
description={t("policyAuthSsoDescription")}
checked={sso}
disabled={disabled}
onCheckedChange={onSsoChange}
/>
</SettingsFormCell>
{!ssoLocked && (
<SettingsFormCell span="full">
<SwitchInput
id="policy-auth-sso"
label={ssoTitle}
description={ssoDescription}
checked={ssoActive}
disabled={disabled}
onCheckedChange={onSsoChange}
/>
</SettingsFormCell>
)}
{sso && (
{ssoActive && (
<>
{ssoLocked && ssoDescription && (
<SettingsFormCell span="full">
<p className="text-sm text-muted-foreground">
{ssoDescription}
</p>
</SettingsFormCell>
)}
<SettingsFormCell span="full">
<FormItem>
<FormLabel>{t("roles")}</FormLabel>
@@ -15,6 +15,7 @@ import {
} from "@app/components/roles-selector";
import { UsersSelector } from "@app/components/users-selector";
import { Button } from "@app/components/ui/button";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import { Form, FormField } from "@app/components/ui/form";
import { toast } from "@app/hooks/useToast";
import { useEnvContext } from "@app/hooks/useEnvContext";
@@ -28,8 +29,10 @@ import type { GetResourcePolicyResponse } from "@server/routers/policy";
import { UserType } from "@server/types/UserTypes";
import { useQuery } from "@tanstack/react-query";
import type { AxiosResponse } from "axios";
import { ExternalLink, InfoIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { toUnicode } from "punycode";
import {
useActionState,
useContext,
@@ -108,9 +111,21 @@ export function PolicyAuthStackSectionEdit({
const resourceContext = useContext(ResourceContext);
const api = createApiClient(useEnvContext());
const isInferenceResource = resourceContext?.resource.mode === "inference";
const isResourceOverlay = resourceId !== undefined;
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[]>(
() =>
policy.roles.map((r) => ({
@@ -264,6 +279,12 @@ export function PolicyAuthStackSectionEdit({
const overlayRoles = combinedRoles.filter((r) => !r.isAdmin);
const overlayUsers = combinedUsers;
useEffect(() => {
if (isInferenceResource && !form.getValues("sso")) {
form.setValue("sso", true);
}
}, [isInferenceResource, form]);
const [, formAction, isSubmitting] = useActionState(onSubmit, null);
const [isSavingOverlay, setIsSavingOverlay] = useState(false);
@@ -294,7 +315,7 @@ export function PolicyAuthStackSectionEdit({
.put(
`/resource-policy/${policy.resourcePolicyId}/access-control`,
{
sso: payload.sso,
sso: isInferenceResource ? true : payload.sso,
userIds: payload.users.map((user) => user.id),
roleIds: payload.roles.map((role) => Number(role.id)),
skipToIdpId: payload.skipToIdpId
@@ -302,94 +323,101 @@ export function PolicyAuthStackSectionEdit({
)
.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(
api
.put(
`/resource-policy/${policy.resourcePolicyId}/password`,
{ password: payload.password.password }
`/resource-policy/${policy.resourcePolicyId}/whitelist`,
{
emailWhitelistEnabled:
payload.emailWhitelistEnabled,
emails: payload.emails?.map((e) => e.text) ?? []
}
)
.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;
policyUpdates.emailWhitelistEnabled = payload.emailWhitelistEnabled;
}
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 {
const results = await Promise.all(requests);
if (results.every((res) => res && res.status === 200)) {
@@ -411,13 +439,17 @@ export function PolicyAuthStackSectionEdit({
updatePolicy(policyUpdates);
resourceContext?.updateAuthInfo({
sso: payload.sso,
whitelist: payload.emailWhitelistEnabled,
password: passcodeOnServerRef.current,
pincode: pincodeOnServerRef.current,
headerAuth: headerAuthOnServerRef.current
});
resourceContext?.updateAuthInfo(
isInferenceResource
? { sso: true }
: {
sso: payload.sso,
whitelist: payload.emailWhitelistEnabled,
password: passcodeOnServerRef.current,
pincode: pincodeOnServerRef.current,
headerAuth: headerAuthOnServerRef.current
}
);
toast({
title: t("success"),
@@ -514,19 +546,53 @@ export function PolicyAuthStackSectionEdit({
{t("policyAuthStackTitle")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("policyAuthStackDescription")}
{isInferenceResource
? t("policyAuthInferenceStackDescription")
: t("policyAuthStackDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
{isResourceOverlay && (
<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">
<PolicyAuthSsoSection
sso={Boolean(sso)}
onSsoChange={(active) =>
form.setValue("sso", active)
}
sso={Boolean(sso) || isInferenceResource}
onSsoChange={(active) => {
if (isInferenceResource) {
return;
}
form.setValue("sso", active);
}}
skipToIdpId={skipToIdpId}
onSkipToIdpChange={(id) =>
form.setValue("skipToIdpId", id)
@@ -534,6 +600,12 @@ export function PolicyAuthStackSectionEdit({
allIdps={allIdps}
disabled={authReadonly}
idpDisabled={authReadonly}
ssoLocked={isInferenceResource}
description={
isInferenceResource
? t("policyAuthInferenceSsoDescription")
: undefined
}
rolesEditor={
isResourceOverlay ? (
<RolesSelector
@@ -605,100 +677,132 @@ export function PolicyAuthStackSectionEdit({
}
/>
<PolicyAuthOtherMethodsSection
pinActive={pinActive}
passcodeActive={passcodeActive}
emailWhitelistEnabled={Boolean(
emailWhitelistEnabled
)}
headerAuthActive={headerAuthActive}
headerAuthUser={headerAuth?.user ?? ""}
emailCount={emails.length}
emailEnabled={emailEnabled}
disabled={authReadonly}
onConfigure={openMethodEditor}
onTogglePincode={(active) =>
handleToggle("pincode", active, () => {
setPinActive(false);
form.setValue("pincode", null);
})
}
onTogglePasscode={(active) =>
handleToggle("passcode", active, () => {
setPasscodeActive(false);
form.setValue("password", null);
})
}
onToggleEmail={(active) =>
handleToggle("email", active, () =>
form.setValue(
"emailWhitelistEnabled",
false
{!isInferenceResource && (
<PolicyAuthOtherMethodsSection
pinActive={pinActive}
passcodeActive={passcodeActive}
emailWhitelistEnabled={Boolean(
emailWhitelistEnabled
)}
headerAuthActive={headerAuthActive}
headerAuthUser={headerAuth?.user ?? ""}
emailCount={emails.length}
emailEnabled={emailEnabled}
disabled={authReadonly}
onConfigure={openMethodEditor}
onTogglePincode={(active) =>
handleToggle("pincode", active, () => {
setPinActive(false);
form.setValue("pincode", null);
})
}
onTogglePasscode={(active) =>
handleToggle("passcode", active, () => {
setPasscodeActive(false);
form.setValue("password", null);
})
}
onToggleEmail={(active) =>
handleToggle("email", active, () =>
form.setValue(
"emailWhitelistEnabled",
false
)
)
)
}
onToggleHeaderAuth={(active) =>
handleToggle("headerAuth", active, () => {
setHeaderAuthActive(false);
form.setValue("headerAuth", null);
})
}
/>
}
onToggleHeaderAuth={(active) =>
handleToggle(
"headerAuth",
active,
() => {
setHeaderAuthActive(false);
form.setValue(
"headerAuth",
null
);
}
)
}
/>
)}
</SettingsSectionForm>
<PincodeCredenza
open={editingMethod === "pincode"}
onOpenChange={(open) => !open && closeCredenza()}
defaultPincode={pincode?.pincode ?? ""}
onSave={(value) => {
form.setValue("pincode", { pincode: value });
setPinActive(true);
}}
/>
{!isInferenceResource && (
<>
<PincodeCredenza
open={editingMethod === "pincode"}
onOpenChange={(open) =>
!open && closeCredenza()
}
defaultPincode={pincode?.pincode ?? ""}
onSave={(value) => {
form.setValue("pincode", {
pincode: value
});
setPinActive(true);
}}
/>
<PasscodeCredenza
open={editingMethod === "passcode"}
onOpenChange={(open) => !open && closeCredenza()}
defaultPassword={password?.password ?? ""}
existingConfigured={Boolean(policy.passwordId)}
onSave={(value) => {
form.setValue("password", { password: value });
setPasscodeActive(true);
}}
/>
<PasscodeCredenza
open={editingMethod === "passcode"}
onOpenChange={(open) =>
!open && closeCredenza()
}
defaultPassword={password?.password ?? ""}
existingConfigured={Boolean(
policy.passwordId
)}
onSave={(value) => {
form.setValue("password", {
password: value
});
setPasscodeActive(true);
}}
/>
<EmailCredenza
open={editingMethod === "email"}
onOpenChange={(open) => !open && closeCredenza()}
emailEnabled={emailEnabled}
disabled={authReadonly}
emails={emails}
onSave={(value) => {
form.setValue("emails", value);
form.setValue("emailWhitelistEnabled", true);
}}
/>
<EmailCredenza
open={editingMethod === "email"}
onOpenChange={(open) =>
!open && closeCredenza()
}
emailEnabled={emailEnabled}
disabled={authReadonly}
emails={emails}
onSave={(value) => {
form.setValue("emails", value);
form.setValue(
"emailWhitelistEnabled",
true
);
}}
/>
<HeaderAuthCredenza
open={editingMethod === "headerAuth"}
onOpenChange={(open) => !open && closeCredenza()}
defaultValues={
headerAuth
? {
user: headerAuth.user,
password: headerAuth.password,
extendedCompatibility:
headerAuth.extendedCompatibility ??
true
}
: undefined
}
existingConfigured={Boolean(policy.headerAuth?.id)}
onSave={(value) => {
form.setValue("headerAuth", value);
setHeaderAuthActive(true);
}}
/>
<HeaderAuthCredenza
open={editingMethod === "headerAuth"}
onOpenChange={(open) =>
!open && closeCredenza()
}
defaultValues={
headerAuth
? {
user: headerAuth.user,
password: headerAuth.password,
extendedCompatibility:
headerAuth.extendedCompatibility ??
true
}
: undefined
}
existingConfigured={Boolean(
policy.headerAuth?.id
)}
onSave={(value) => {
form.setValue("headerAuth", value);
setHeaderAuthActive(true);
}}
/>
</>
)}
</SettingsSectionBody>
<SettingsSectionFooter>
<Button