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

This commit is contained in:
Owen
2026-08-12 11:45:28 -04:00
9 changed files with 68 additions and 60 deletions
+5 -5
View File
@@ -1699,16 +1699,16 @@
"virtualApiKeysFilterUnassigned": "Unassigned", "virtualApiKeysFilterUnassigned": "Unassigned",
"myVirtualApiKeysTitle": "Your API Keys", "myVirtualApiKeysTitle": "Your API Keys",
"myVirtualApiKeysDescription": "View your identity key and any virtual API keys attributed to you in this organization", "myVirtualApiKeysDescription": "View your identity key and any virtual API keys attributed to you in this organization",
"myVirtualApiKeysResourceTitle": "Your API Keys for This Resource", "myVirtualApiKeysResourceTitle": "Your API Keys for {resourceName}",
"myVirtualApiKeysResourceDescription": "View your identity key and virtual API keys attributed to you that can access this resource", "myVirtualApiKeysResourceDescription": "View your identity key and virtual API keys attributed to you that can access {resourceName}",
"myVirtualApiKeysIdentityTitle": "Identity Key", "myVirtualApiKeysIdentityTitle": "Identity Key",
"myVirtualApiKeysIdentityHeadline": "Your Personal API Key", "myVirtualApiKeysIdentityHeadline": "Your Personal API Key",
"myVirtualApiKeysIdentityDescription": "Your personal key for this organization. It is unique to your account and used to identify you when calling AI Gateway resources.", "myVirtualApiKeysIdentityDescription": "Your personal key for this organization. It is unique to your account and used to identify you when calling AI Gateway resources.",
"myVirtualApiKeysIdentityResourceHeadline": "Your Personal API Key for This Resource", "myVirtualApiKeysIdentityResourceHeadline": "Your Personal API Key for {resourceName}",
"myVirtualApiKeysIdentityResourceDescription": "Your personal key for this organization. Use it to call this AI Gateway resource.", "myVirtualApiKeysIdentityResourceDescription": "Your personal key for this organization. Use it to call {resourceName}.",
"myVirtualApiKeysManualTitle": "Attributed Keys", "myVirtualApiKeysManualTitle": "Attributed Keys",
"myVirtualApiKeysManualDescription": "Manual virtual API keys an admin associated with your account", "myVirtualApiKeysManualDescription": "Manual virtual API keys an admin associated with your account",
"myVirtualApiKeysManualResourceDescription": "Manual virtual API keys associated with your account that can access this resource", "myVirtualApiKeysManualResourceDescription": "Manual virtual API keys associated with your account that can access {resourceName}",
"myVirtualApiKeysManualEmpty": "No attributed keys yet", "myVirtualApiKeysManualEmpty": "No attributed keys yet",
"myVirtualApiKeysKindUser": "Identity", "myVirtualApiKeysKindUser": "Identity",
"myVirtualApiKeysKindManual": "Manual", "myVirtualApiKeysKindManual": "Manual",
+13 -21
View File
@@ -263,28 +263,20 @@ export async function verifyResourceSession(
); );
if (action == "ACCEPT") { if (action == "ACCEPT") {
// Public inference still requires a virtual API key; do not logger.debug("Resource allowed by rule");
// bypass that with an allow rule.
if (mode === "inference") {
logger.debug(
"Rule ACCEPT ignored for inference; continuing to virtual API key check"
);
} else {
logger.debug("Resource allowed by rule");
logRequestAudit( logRequestAudit(
{ {
action: true, action: true,
reason: 100, // allowed by rule reason: 100, // allowed by rule
resourceId: resource.resourceId, resourceId: resource.resourceId,
orgId: resource.orgId, orgId: resource.orgId,
location: ipCC location: ipCC
}, },
parsedBody.data parsedBody.data
); );
return allowed(res, undefined, dontStripSession); return allowed(res, undefined, dontStripSession);
}
} else if (action == "DROP") { } else if (action == "DROP") {
logger.debug("Resource denied by rule"); logger.debug("Resource denied by rule");
@@ -425,7 +417,7 @@ export async function verifyResourceSession(
parsedBody.data parsedBody.data
); );
return notAllowed(res, redirectPath); return notAllowed(res, redirectPath, resource.orgId);
} }
// check for access token in headers // check for access token in headers
@@ -42,6 +42,7 @@ export type GetResourceAuthInfoResponse = {
skipToIdpId: number | null; skipToIdpId: number | null;
orgId: string; orgId: string;
postAuthPath: string | null; postAuthPath: string | null;
mode: string;
}; };
export async function getResourceAuthInfo( export async function getResourceAuthInfo(
@@ -227,7 +228,8 @@ export async function getResourceAuthInfo(
whitelist: effectivePolicy?.emailWhitelistEnabled ?? false, whitelist: effectivePolicy?.emailWhitelistEnabled ?? false,
skipToIdpId: effectivePolicy?.idpId ?? resource.skipToIdpId, skipToIdpId: effectivePolicy?.idpId ?? resource.skipToIdpId,
orgId: resource.orgId, orgId: resource.orgId,
postAuthPath: resource.postAuthPath ?? null postAuthPath: resource.postAuthPath ?? null,
mode: resource.mode
}, },
success: true, success: true,
error: false, error: false,
@@ -123,10 +123,12 @@ export async function listMyVirtualApiKeys(
} }
let resourceId: number | undefined; let resourceId: number | undefined;
let resourceName: string | undefined;
if (resourceGuid) { if (resourceGuid) {
const [resource] = await db const [resource] = await db
.select({ .select({
resourceId: resources.resourceId resourceId: resources.resourceId,
name: resources.name
}) })
.from(resources) .from(resources)
.where( .where(
@@ -147,6 +149,7 @@ export async function listMyVirtualApiKeys(
} }
resourceId = resource.resourceId; resourceId = resource.resourceId;
resourceName = resource.name;
} }
const { key: userKeyRow } = await getOrCreateUserVirtualApiKey({ const { key: userKeyRow } = await getOrCreateUserVirtualApiKey({
@@ -203,7 +206,8 @@ export async function listMyVirtualApiKeys(
userKey: toKeyWithResources(userKeyRow, resourceIdsByKey), userKey: toKeyWithResources(userKeyRow, resourceIdsByKey),
manualKeys: manualRows.map((row) => manualKeys: manualRows.map((row) =>
toKeyWithResources(row, resourceIdsByKey) toKeyWithResources(row, resourceIdsByKey)
) ),
...(resourceName !== undefined ? { resourceName } : {})
}, },
success: true, success: true,
error: false, error: false,
+1
View File
@@ -22,6 +22,7 @@ export type CreateOrEditVirtualApiKeyResponse = {
export type ListMyVirtualApiKeysResponse = { export type ListMyVirtualApiKeysResponse = {
userKey: VirtualApiKeyWithResources; userKey: VirtualApiKeyWithResources;
manualKeys: VirtualApiKeyWithResources[]; manualKeys: VirtualApiKeyWithResources[];
resourceName?: string | null;
}; };
export type GetMyVirtualApiKeyResponse = { export type GetMyVirtualApiKeyResponse = {
@@ -18,7 +18,7 @@ import { cache } from "react";
export async function generateMetadata(): Promise<Metadata> { export async function generateMetadata(): Promise<Metadata> {
const t = await getTranslations(); const t = await getTranslations();
return { return {
title: t("myVirtualApiKeysResourceTitle") title: t("myVirtualApiKeysTitle")
}; };
} }
@@ -41,7 +41,9 @@ export default async function ResourceKeysPage(props: ResourceKeysPageProps) {
const user = await getUser(); const user = await getUser();
if (!user) { if (!user) {
redirect("/"); redirect(
`/auth/resource/${encodeURIComponent(resourceGuid)}?redirect=${encodeURIComponent(`/${orgId}/resource/${resourceGuid}/keys`)}`
);
} }
const cookieHeader = await authCookieHeader(); const cookieHeader = await authCookieHeader();
@@ -111,11 +113,7 @@ export default async function ResourceKeysPage(props: ResourceKeysPageProps) {
launcherMode launcherMode
showViewAsAdmin={isAdminOrOwner} showViewAsAdmin={isAdminOrOwner}
> >
<UserVirtualApiKeys <UserVirtualApiKeys orgId={orgId} initialData={keysData} />
orgId={orgId}
resourceGuid={resourceGuid}
initialData={keysData}
/>
</Layout> </Layout>
</UserProvider> </UserProvider>
); );
+18 -3
View File
@@ -71,6 +71,9 @@ export default async function ResourceAuthPage(props: {
); );
} }
const isInference = authInfo.mode === "inference";
const keysPath = `/${authInfo.orgId}/resource/${authInfo.resourceGuid}/keys`;
const hasLoginPageDomain = await isOrgSubscribed( const hasLoginPageDomain = await isOrgSubscribed(
authInfo.orgId, authInfo.orgId,
tierMatrix.loginPageDomain tierMatrix.loginPageDomain
@@ -159,7 +162,9 @@ export default async function ResourceAuthPage(props: {
if (user && !user.emailVerified && env.flags.emailVerificationRequired) { if (user && !user.emailVerified && env.flags.emailVerificationRequired) {
redirect( redirect(
`/auth/verify-email?redirect=/auth/resource/${authInfo.resourceGuid}` `/auth/verify-email?redirect=${encodeURIComponent(
`/auth/resource/${authInfo.resourceGuid}`
)}`
); );
} }
@@ -193,6 +198,16 @@ export default async function ResourceAuthPage(props: {
); );
} }
// Inference resources never establish a resource session on the inference
// host. Authenticated users retrieve their virtual API key on the dashboard.
if (isInference && user) {
redirect(keysPath);
}
// After password/pincode/SSO, do not send the browser back to the
// inference host (session alone cannot pass Badger). Land on keys instead.
const postAuthRedirect = isInference ? keysPath : redirectUrl;
if (!hasAuth) { if (!hasAuth) {
// no authentication so always go straight to the resource // no authentication so always go straight to the resource
redirect(redirectUrl); redirect(redirectUrl);
@@ -277,7 +292,7 @@ export default async function ResourceAuthPage(props: {
<AutoLoginHandler <AutoLoginHandler
resourceId={authInfo.resourceId} resourceId={authInfo.resourceId}
skipToIdpId={authInfo.skipToIdpId} skipToIdpId={authInfo.skipToIdpId}
redirectUrl={redirectUrl} redirectUrl={postAuthRedirect}
orgId={build === "saas" ? authInfo.orgId : undefined} orgId={build === "saas" ? authInfo.orgId : undefined}
/> />
); );
@@ -315,7 +330,7 @@ export default async function ResourceAuthPage(props: {
name: authInfo.resourceName, name: authInfo.resourceName,
id: authInfo.resourceId id: authInfo.resourceId
}} }}
redirect={redirectUrl} redirect={postAuthRedirect}
idps={loginIdps} idps={loginIdps}
orgId={build === "saas" ? authInfo.orgId : undefined} orgId={build === "saas" ? authInfo.orgId : undefined}
branding={ branding={
+11 -20
View File
@@ -4,11 +4,9 @@ import { useState } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import moment from "moment"; import moment from "moment";
import { Badge } from "@app/components/ui/badge";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import CopyTextBox from "@app/components/CopyTextBox"; import CopyTextBox from "@app/components/CopyTextBox";
import CopyToClipboard from "@app/components/CopyToClipboard"; import CopyToClipboard from "@app/components/CopyToClipboard";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { import {
SettingsContainer, SettingsContainer,
SettingsFormCell, SettingsFormCell,
@@ -34,7 +32,6 @@ import {
type UserVirtualApiKeysProps = { type UserVirtualApiKeysProps = {
orgId: string; orgId: string;
resourceGuid?: string;
initialData: ListMyVirtualApiKeysResponse; initialData: ListMyVirtualApiKeysResponse;
}; };
@@ -131,12 +128,12 @@ function IdentityKeyCenterpiece({
orgId, orgId,
virtualApiKeyId, virtualApiKeyId,
lastChars, lastChars,
resourceGuid resourceName
}: { }: {
orgId: string; orgId: string;
virtualApiKeyId: string; virtualApiKeyId: string;
lastChars: string; lastChars: string;
resourceGuid?: string; resourceName?: string | null;
}) { }) {
const t = useTranslations(); const t = useTranslations();
const preview = formatVirtualApiKeyPreview(virtualApiKeyId, lastChars); const preview = formatVirtualApiKeyPreview(virtualApiKeyId, lastChars);
@@ -145,11 +142,11 @@ function IdentityKeyCenterpiece({
virtualApiKeyId virtualApiKeyId
); );
const displayValue = credential ?? preview; const displayValue = credential ?? preview;
const headline = resourceGuid const headline = resourceName
? t("myVirtualApiKeysIdentityResourceHeadline") ? t("myVirtualApiKeysIdentityResourceHeadline", { resourceName })
: t("myVirtualApiKeysIdentityHeadline"); : t("myVirtualApiKeysIdentityHeadline");
const description = resourceGuid const description = resourceName
? t("myVirtualApiKeysIdentityResourceDescription") ? t("myVirtualApiKeysIdentityResourceDescription", { resourceName })
: t("myVirtualApiKeysIdentityDescription"); : t("myVirtualApiKeysIdentityDescription");
return ( return (
@@ -220,17 +217,10 @@ function ManualKeyRow({
export default function UserVirtualApiKeys({ export default function UserVirtualApiKeys({
orgId, orgId,
resourceGuid,
initialData initialData
}: UserVirtualApiKeysProps) { }: UserVirtualApiKeysProps) {
const t = useTranslations(); const t = useTranslations();
const resourceName = initialData.resourceName;
const title = resourceGuid
? t("myVirtualApiKeysResourceTitle")
: t("myVirtualApiKeysTitle");
const description = resourceGuid
? t("myVirtualApiKeysResourceDescription")
: t("myVirtualApiKeysDescription");
return ( return (
<> <>
@@ -239,7 +229,7 @@ export default function UserVirtualApiKeys({
orgId={orgId} orgId={orgId}
virtualApiKeyId={initialData.userKey.virtualApiKeyId} virtualApiKeyId={initialData.userKey.virtualApiKeyId}
lastChars={initialData.userKey.lastChars} lastChars={initialData.userKey.lastChars}
resourceGuid={resourceGuid} resourceName={resourceName}
/> />
{initialData.manualKeys.length > 0 ? ( {initialData.manualKeys.length > 0 ? (
@@ -249,9 +239,10 @@ export default function UserVirtualApiKeys({
{t("myVirtualApiKeysManualTitle")} {t("myVirtualApiKeysManualTitle")}
</SectionTitle> </SectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{resourceGuid {resourceName
? t( ? t(
"myVirtualApiKeysManualResourceDescription" "myVirtualApiKeysManualResourceDescription",
{ resourceName }
) )
: t("myVirtualApiKeysManualDescription")} : t("myVirtualApiKeysManualDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
@@ -202,7 +202,12 @@ export function LauncherInferenceApiKeysSection({
</SettingsSubsectionTitle> </SettingsSubsectionTitle>
<SettingsSubsectionDescription> <SettingsSubsectionDescription>
{t( {t(
"myVirtualApiKeysManualResourceDescription" "myVirtualApiKeysManualResourceDescription",
{
resourceName:
data.resourceName ??
t("resource")
}
)} )}
</SettingsSubsectionDescription> </SettingsSubsectionDescription>
</SettingsSubsectionHeader> </SettingsSubsectionHeader>