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 (
-
- column.toggleSorting(column.getIsSorted() === "asc")
- }
- >
- {t("expires")}
-
-
- );
- },
- 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);
+ }}
+ />
+ >
+ )}