Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
Typography,
} from '@wso2/oxygen-ui';
import { CheckCircle2, Copy, FlaskConical } from '@wso2/oxygen-ui-icons-react';
import { formatPrefixedKey } from '../../utils/apiKeyAuthDisplay';

interface EndpointOption {
path: string;
Expand Down Expand Up @@ -116,6 +117,7 @@ interface Props {
gatewayUrl: string;
apiKeyHeaderName: string;
apiKeyLocation: 'header' | 'query';
apiKeyValuePrefix?: string;
providerTemplate?: string | null;
}

Expand All @@ -130,7 +132,7 @@ function buildCurlCommand(
const bodyJson = JSON.stringify(body, null, 2);
const fullUrl =
apiKeyLocation === 'query'
? `${url}${url.includes('?') ? '&' : '?'}${apiKeyHeaderName}=${apiKey}`
? `${url}${url.includes('?') ? '&' : '?'}${apiKeyHeaderName}=${encodeURIComponent(apiKey)}`
: url;

const lines = [
Expand All @@ -157,6 +159,7 @@ export default function ApiTryOutCurlSnippet({
gatewayUrl,
apiKeyHeaderName,
apiKeyLocation,
apiKeyValuePrefix,
providerTemplate,
}: Props) {
const endpoint = useMemo(() => {
Expand All @@ -169,8 +172,9 @@ export default function ApiTryOutCurlSnippet({
const curlCommand = useMemo(() => {
const base = gatewayUrl ? gatewayUrl.replace(/\/+$/, '') : '<gateway-url>';
const url = `${base}${endpoint.path}`;
return buildCurlCommand(url, apiKeyHeaderName, apiKeyLocation, apiKey, endpoint.body, endpoint.extraHeaders);
}, [apiKey, apiKeyHeaderName, apiKeyLocation, gatewayUrl, endpoint]);
const keyValue = formatPrefixedKey(apiKeyValuePrefix ?? '', apiKey);
return buildCurlCommand(url, apiKeyHeaderName, apiKeyLocation, keyValue, endpoint.body, endpoint.extraHeaders);
}, [apiKey, apiKeyHeaderName, apiKeyLocation, apiKeyValuePrefix, gatewayUrl, endpoint]);

const handleCopy = async () => {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
getProjectSlug,
} from '../../../../utils/projectRouting';
import { truncateProviderDisplayName } from '../../../../utils/providerTemplateDisplay';
import { resolveApiKeyAuthDisplay } from '../../../../utils/apiKeyAuthDisplay';
import type { CreateProxyRequest, LLMProvider } from '../../../../utils/types';
import { useAIWorkspaceSnackbar } from '../../../../hooks/aiWorkspaceSnackbar';
import { logger } from '../../../../utils/logger';
Expand Down Expand Up @@ -279,8 +280,10 @@ function LLMProxyNewContent({
const selectedProviderRequiresApiKey = Boolean(
providerDetail?.security?.enabled && providerDetail.security.apiKey?.enabled
);
const selectedProviderApiKeyName =
providerDetail?.security?.apiKey?.key?.trim() || 'X-API-Key';
const selectedProviderApiKeyName = resolveApiKeyAuthDisplay(
providerDetail?.security,
providerDetail?.globalPolicies
).headerName;
const projectSlug = getProjectSlug(effectiveProject);
const generatedProxyId = toProxyId(formState.name);
const computedContext = projectSlug
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ import {
GATEWAY_MANAGED_ARTIFACT_TOOLTIP,
} from '../../../../utils/readOnlyArtifacts';
import ApiTryOutCurlSnippet from '../../../../Components/common/ApiTryOutCurlSnippet';
import {
formatPrefixedKey,
resolveApiKeyAuthDisplay,
} from '../../../../utils/apiKeyAuthDisplay';

type OpenApiSpec = Record<string, unknown>;

Expand Down Expand Up @@ -130,8 +134,14 @@ export default function LLMProxyOverviewTab() {
// available even for gateway-created (read-only) proxies.
const isReadOnlyProxy = false;

const apiKeyLocation = proxy?.security?.apiKey?.in ?? 'header';
const apiKeyName = proxy?.security?.apiKey?.key ?? 'X-API-Key';
const {
headerName: apiKeyName,
location: apiKeyLocation,
valuePrefix: apiKeyValuePrefix,
} = useMemo(
() => resolveApiKeyAuthDisplay(proxy?.security, proxy?.globalPolicies),
[proxy?.security, proxy?.globalPolicies]
);
const providerTemplate = useMemo(() => {
const providerId =
typeof proxy?.provider === 'string'
Expand Down Expand Up @@ -194,9 +204,12 @@ export default function LLMProxyOverviewTab() {
const resolvedApiKeyHeaderName = apiKeyName.trim() || 'X-API-Key';

return {
[resolvedApiKeyHeaderName]: latestGeneratedKey,
[resolvedApiKeyHeaderName]: formatPrefixedKey(
apiKeyValuePrefix,
latestGeneratedKey
),
};
}, [apiKeyName, latestGeneratedKey]);
}, [apiKeyName, apiKeyValuePrefix, latestGeneratedKey]);

const swaggerViewerKey = useMemo(
() =>
Expand Down Expand Up @@ -934,6 +947,7 @@ export default function LLMProxyOverviewTab() {
gatewayUrl={generatedGatewayUrl}
apiKeyHeaderName={apiKeyName}
apiKeyLocation={apiKeyLocation}
apiKeyValuePrefix={apiKeyValuePrefix}
providerTemplate={providerTemplate}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ import { FormattedMessage } from 'react-intl';
import useAIWorkspaceSnackbar from '../../../../hooks/aiWorkspaceSnackbar';
import SwaggerSpecViewer from '../../../../Components/SwaggerSpecViewer';
import { buildProjectPath } from '../../../../utils/projectRouting';
import {
formatPrefixedKey,
resolveApiKeyAuthDisplay,
} from '../../../../utils/apiKeyAuthDisplay';
import {
DisabledActionTooltip,
GATEWAY_MANAGED_ARTIFACT_TOOLTIP,
Expand Down Expand Up @@ -156,8 +160,14 @@ export default function ServiceProviderOverviewTab({
const [isDeletingKey, setIsDeletingKey] = useState(false);
const [apiKeys, setApiKeys] = useState<UserAPIKey[]>([]);
const [keysLoading, setKeysLoading] = useState(false);
const apiKeyLocation = provider?.security?.apiKey?.in ?? 'header';
const apiKeyName = provider?.security?.apiKey?.key ?? 'X-API-Key';
const {
headerName: apiKeyName,
location: apiKeyLocation,
valuePrefix: apiKeyValuePrefix,
} = useMemo(
() => resolveApiKeyAuthDisplay(provider?.security, provider?.globalPolicies),
[provider?.security, provider?.globalPolicies]
);
const showSnackbar = useAIWorkspaceSnackbar();
const isReadOnlyProvider = Boolean(provider?.readOnly);

Expand Down Expand Up @@ -211,9 +221,12 @@ export default function ServiceProviderOverviewTab({
const resolvedApiKeyHeaderName = apiKeyName.trim() || 'X-API-Key';

return {
[resolvedApiKeyHeaderName]: latestGeneratedKey,
[resolvedApiKeyHeaderName]: formatPrefixedKey(
apiKeyValuePrefix,
latestGeneratedKey
),
};
}, [apiKeyName, latestGeneratedKey]);
}, [apiKeyName, apiKeyValuePrefix, latestGeneratedKey]);
const swaggerViewerKey = useMemo(
() =>
[
Expand Down Expand Up @@ -1046,6 +1059,7 @@ export default function ServiceProviderOverviewTab({
gatewayUrl={generatedGatewayUrl}
apiKeyHeaderName={apiKeyName}
apiKeyLocation={apiKeyLocation}
apiKeyValuePrefix={apiKeyValuePrefix}
providerTemplate={provider?.template}
/>
</>
Expand Down
94 changes: 94 additions & 0 deletions portals/ai-workspace/src/utils/apiKeyAuthDisplay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import type { GlobalPolicy } from './types';

export interface ApiKeyAuthDisplay {
headerName: string;
location: 'header' | 'query';
valuePrefix: string;
}

interface ApiKeySecurityLike {
enabled?: boolean;
key?: string;
in?: 'header' | 'query';
valuePrefix?: string;
}

interface SecurityConfigLike {
enabled?: boolean;
apiKey?: ApiKeySecurityLike;
}

const API_KEY_AUTH_POLICY = 'api-key-auth';
const DEFAULT_HEADER_NAME = 'X-API-Key';

/**
* Resolve the API key auth settings that are actually in effect for a
* provider/proxy, mirroring the deployment precedence: the first-class
* security config wins; otherwise an attached api-key-auth global policy;
* otherwise the platform defaults. Used by the key-generation dialogs and
* try-it-out curl snippets so they reflect the deployed configuration.
*/
export function resolveApiKeyAuthDisplay(
security?: SecurityConfigLike | null,
globalPolicies?: GlobalPolicy[] | null
): ApiKeyAuthDisplay {
const apiKey = security?.apiKey;
if (security?.enabled !== false && apiKey && apiKey.enabled !== false && apiKey.key?.trim()) {
return {
headerName: apiKey.key.trim(),
location: apiKey.in ?? 'header',
valuePrefix: apiKey.valuePrefix ?? '',
};
}

const policy = globalPolicies?.find((p) => p.name === API_KEY_AUTH_POLICY);
if (policy?.params) {
const key = typeof policy.params.key === 'string' ? policy.params.key.trim() : '';
const location = policy.params.in === 'query' ? 'query' : 'header';
const valuePrefix =
typeof policy.params.valuePrefix === 'string' ? policy.params.valuePrefix : '';
return {
headerName: key || DEFAULT_HEADER_NAME,
location,
valuePrefix,
};
}

// Fall back to security config even if disabled flags are ambiguous
if (apiKey?.key?.trim()) {
return {
headerName: apiKey.key.trim(),
location: apiKey.in ?? 'header',
valuePrefix: apiKey.valuePrefix ?? '',
};
}

return { headerName: DEFAULT_HEADER_NAME, location: 'header', valuePrefix: '' };
}

/**
* Build the header/query value a client must send: the configured prefix
* (single-space separated, e.g. "Bearer <key>") followed by the key.
*/
export function formatPrefixedKey(valuePrefix: string, apiKey: string): string {
const prefix = valuePrefix.trimEnd();
return prefix ? `${prefix} ${apiKey}` : apiKey;
}
Loading