diff --git a/messages/en-US.json b/messages/en-US.json index 164f110e7..d5ecdf780 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -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 , 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", diff --git a/src/components/VirtualApiKeysTable.tsx b/src/components/VirtualApiKeysTable.tsx index d5c2d3717..074d82f74 100644 --- a/src/components/VirtualApiKeysTable.tsx +++ b/src/components/VirtualApiKeysTable.tsx @@ -372,30 +372,6 @@ export default function VirtualApiKeysTable({ }, cell: ({ row }) => moment(row.original.createdAt).format("lll") }, - { - accessorKey: "expiresAt", - friendlyName: t("expires"), - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const expiresAt = row.original.expiresAt; - if (expiresAt) { - return moment(expiresAt).format("lll"); - } - return t("never"); - } - }, { id: "actions", enableHiding: false, diff --git a/src/components/resource-policy/PolicyAuthSsoSection.tsx b/src/components/resource-policy/PolicyAuthSsoSection.tsx index f7f8b3110..3cc2e54c4 100644 --- a/src/components/resource-policy/PolicyAuthSsoSection.tsx +++ b/src/components/resource-policy/PolicyAuthSsoSection.tsx @@ -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 ( - - - + {!ssoLocked && ( + + + + )} - {sso && ( + {ssoActive && ( <> + {ssoLocked && ssoDescription && ( + +

+ {ssoDescription} +

+
+ )} {t("roles")} diff --git a/src/components/resource-policy/PolicyAuthStackSectionEdit.tsx b/src/components/resource-policy/PolicyAuthStackSectionEdit.tsx index aa7700948..51b718e88 100644 --- a/src/components/resource-policy/PolicyAuthStackSectionEdit.tsx +++ b/src/components/resource-policy/PolicyAuthStackSectionEdit.tsx @@ -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( () => 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")} - {t("policyAuthStackDescription")} + {isInferenceResource + ? t("policyAuthInferenceStackDescription") + : t("policyAuthStackDescription")} {isResourceOverlay && ( )} + {isInferenceResource && ( + + + + {inferenceResourceUrl + ? t.rich( + "policyAuthInferenceIdentityKeyHelp", + { + resourceLink: () => ( + + {inferenceResourceUrl} + + + ) + } + ) + : t( + "policyAuthInferenceIdentityKeyHelpNoUrl" + )} + + + )} - 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 ? ( - - 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 && ( + + 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 + ); + } + ) + } + /> + )} - !open && closeCredenza()} - defaultPincode={pincode?.pincode ?? ""} - onSave={(value) => { - form.setValue("pincode", { pincode: value }); - setPinActive(true); - }} - /> + {!isInferenceResource && ( + <> + + !open && closeCredenza() + } + defaultPincode={pincode?.pincode ?? ""} + onSave={(value) => { + form.setValue("pincode", { + pincode: value + }); + setPinActive(true); + }} + /> - !open && closeCredenza()} - defaultPassword={password?.password ?? ""} - existingConfigured={Boolean(policy.passwordId)} - onSave={(value) => { - form.setValue("password", { password: value }); - setPasscodeActive(true); - }} - /> + + !open && closeCredenza() + } + defaultPassword={password?.password ?? ""} + existingConfigured={Boolean( + policy.passwordId + )} + onSave={(value) => { + form.setValue("password", { + password: value + }); + setPasscodeActive(true); + }} + /> - !open && closeCredenza()} - emailEnabled={emailEnabled} - disabled={authReadonly} - emails={emails} - onSave={(value) => { - form.setValue("emails", value); - form.setValue("emailWhitelistEnabled", true); - }} - /> + + !open && closeCredenza() + } + emailEnabled={emailEnabled} + disabled={authReadonly} + emails={emails} + onSave={(value) => { + form.setValue("emails", value); + form.setValue( + "emailWhitelistEnabled", + true + ); + }} + /> - !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); - }} - /> + + !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); + }} + /> + + )}