mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
feat(lighthouse): Support Amazon Bedrock Long-Term API Key (#9343)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
cb84bd0f94
commit
26fd7d3adc
@@ -14,21 +14,47 @@ import type { LighthouseProvider } from "@/types/lighthouse";
|
||||
import { getMainFields, getProviderConfig } from "./llm-provider-registry";
|
||||
import {
|
||||
isProviderFormValid,
|
||||
type LLMCredentialsFormData,
|
||||
shouldTestConnection,
|
||||
testAndRefreshModels,
|
||||
} from "./llm-provider-utils";
|
||||
|
||||
const BEDROCK_CREDENTIAL_MODES = {
|
||||
API_KEY: "api_key",
|
||||
IAM: "iam",
|
||||
} as const;
|
||||
|
||||
type BedrockCredentialMode =
|
||||
(typeof BEDROCK_CREDENTIAL_MODES)[keyof typeof BEDROCK_CREDENTIAL_MODES];
|
||||
|
||||
const CONNECTION_STATUS = {
|
||||
IDLE: "idle",
|
||||
CONNECTING: "connecting",
|
||||
VERIFYING: "verifying",
|
||||
LOADING_MODELS: "loading-models",
|
||||
} as const;
|
||||
|
||||
type ConnectionStatus =
|
||||
(typeof CONNECTION_STATUS)[keyof typeof CONNECTION_STATUS];
|
||||
|
||||
const STATUS_MESSAGES: Record<Exclude<ConnectionStatus, "idle">, string> = {
|
||||
[CONNECTION_STATUS.CONNECTING]: "Connecting...",
|
||||
[CONNECTION_STATUS.VERIFYING]: "Verifying...",
|
||||
[CONNECTION_STATUS.LOADING_MODELS]: "Loading models...",
|
||||
};
|
||||
|
||||
interface ConnectLLMProviderProps {
|
||||
provider: LighthouseProvider;
|
||||
mode?: string;
|
||||
initialAuthMode?: BedrockCredentialMode;
|
||||
}
|
||||
|
||||
type FormData = Record<string, string>;
|
||||
type Status = "idle" | "connecting" | "verifying" | "loading-models";
|
||||
|
||||
export const ConnectLLMProvider = ({
|
||||
provider,
|
||||
mode = "create",
|
||||
initialAuthMode,
|
||||
}: ConnectLLMProviderProps) => {
|
||||
const router = useRouter();
|
||||
const providerConfig = getProviderConfig(provider);
|
||||
@@ -38,9 +64,17 @@ export const ConnectLLMProvider = ({
|
||||
const [existingProviderId, setExistingProviderId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [status, setStatus] = useState<Status>("idle");
|
||||
const [status, setStatus] = useState<ConnectionStatus>(
|
||||
CONNECTION_STATUS.IDLE,
|
||||
);
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [bedrockMode, setBedrockMode] = useState<BedrockCredentialMode>(() => {
|
||||
if (provider === "bedrock" && mode !== "edit" && initialAuthMode) {
|
||||
return initialAuthMode;
|
||||
}
|
||||
return BEDROCK_CREDENTIAL_MODES.API_KEY;
|
||||
});
|
||||
|
||||
// Fetch existing provider ID in edit mode
|
||||
useEffect(() => {
|
||||
@@ -56,6 +90,25 @@ export const ConnectLLMProvider = ({
|
||||
);
|
||||
}
|
||||
setExistingProviderId(result.data.id);
|
||||
|
||||
// For Bedrock, detect existing credential mode (API key vs IAM)
|
||||
if (provider === "bedrock") {
|
||||
const attributes = (result.data as any)?.attributes;
|
||||
const credentials = attributes?.credentials as
|
||||
| LLMCredentialsFormData
|
||||
| undefined;
|
||||
|
||||
if (credentials) {
|
||||
if (credentials.api_key) {
|
||||
setBedrockMode(BEDROCK_CREDENTIAL_MODES.API_KEY);
|
||||
} else if (
|
||||
credentials.access_key_id ||
|
||||
credentials.secret_access_key
|
||||
) {
|
||||
setBedrockMode(BEDROCK_CREDENTIAL_MODES.IAM);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
@@ -66,13 +119,30 @@ export const ConnectLLMProvider = ({
|
||||
fetchProvider();
|
||||
}, [isEditMode, provider, providerConfig]);
|
||||
|
||||
const buildPayload = (): Record<string, any> => {
|
||||
if (!providerConfig) return {};
|
||||
const buildBedrockPayload = (): Record<string, any> => {
|
||||
const credentials: LLMCredentialsFormData = {};
|
||||
|
||||
if (bedrockMode === BEDROCK_CREDENTIAL_MODES.API_KEY) {
|
||||
if (formData.api_key) credentials.api_key = formData.api_key;
|
||||
if (formData.region) credentials.region = formData.region;
|
||||
} else {
|
||||
if (formData.access_key_id) {
|
||||
credentials.access_key_id = formData.access_key_id;
|
||||
}
|
||||
if (formData.secret_access_key) {
|
||||
credentials.secret_access_key = formData.secret_access_key;
|
||||
}
|
||||
if (formData.region) credentials.region = formData.region;
|
||||
}
|
||||
|
||||
return Object.keys(credentials).length > 0 ? { credentials } : {};
|
||||
};
|
||||
|
||||
const buildGenericPayload = (): Record<string, any> => {
|
||||
const credentials: Record<string, string> = {};
|
||||
const otherFields: Record<string, string> = {};
|
||||
|
||||
providerConfig.fields.forEach((field) => {
|
||||
providerConfig?.fields.forEach((field) => {
|
||||
if (formData[field.name]) {
|
||||
if (field.requiresConnectionTest) {
|
||||
credentials[field.name] = formData[field.name];
|
||||
@@ -88,11 +158,18 @@ export const ConnectLLMProvider = ({
|
||||
};
|
||||
};
|
||||
|
||||
const buildPayload = (): Record<string, any> => {
|
||||
if (!providerConfig) return {};
|
||||
return provider === "bedrock"
|
||||
? buildBedrockPayload()
|
||||
: buildGenericPayload();
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!providerConfig) return;
|
||||
|
||||
setStatus("connecting");
|
||||
setStatus(CONNECTION_STATUS.CONNECTING);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
@@ -125,11 +202,23 @@ export const ConnectLLMProvider = ({
|
||||
setExistingProviderId(providerId);
|
||||
}
|
||||
|
||||
const shouldTestBedrock =
|
||||
(bedrockMode === BEDROCK_CREDENTIAL_MODES.API_KEY &&
|
||||
!!formData.api_key?.trim()) ||
|
||||
(bedrockMode === BEDROCK_CREDENTIAL_MODES.IAM &&
|
||||
(!!formData.access_key_id?.trim() ||
|
||||
!!formData.secret_access_key?.trim()));
|
||||
|
||||
const shouldTest =
|
||||
provider === "bedrock"
|
||||
? shouldTestBedrock
|
||||
: shouldTestConnection(provider, formData);
|
||||
|
||||
// Test connection if credentials provided
|
||||
if (shouldTestConnection(provider, formData)) {
|
||||
setStatus("verifying");
|
||||
if (shouldTest) {
|
||||
setStatus(CONNECTION_STATUS.VERIFYING);
|
||||
await testAndRefreshModels(providerId);
|
||||
setStatus("loading-models");
|
||||
setStatus(CONNECTION_STATUS.LOADING_MODELS);
|
||||
}
|
||||
|
||||
// Navigate to model selection on success
|
||||
@@ -138,7 +227,7 @@ export const ConnectLLMProvider = ({
|
||||
);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
setStatus("idle");
|
||||
setStatus(CONNECTION_STATUS.IDLE);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -153,16 +242,44 @@ export const ConnectLLMProvider = ({
|
||||
};
|
||||
|
||||
const getLoadingText = () => {
|
||||
if (status === "idle") return "Connecting...";
|
||||
|
||||
const statusText: Record<Exclude<Status, "idle">, string> = {
|
||||
connecting: "Connecting...",
|
||||
verifying: "Verifying...",
|
||||
"loading-models": "Loading models...",
|
||||
};
|
||||
return statusText[status] || "Connecting...";
|
||||
if (status === CONNECTION_STATUS.IDLE) {
|
||||
return "";
|
||||
}
|
||||
return (
|
||||
STATUS_MESSAGES[status] || STATUS_MESSAGES[CONNECTION_STATUS.CONNECTING]
|
||||
);
|
||||
};
|
||||
|
||||
const renderFormField = (
|
||||
id: string,
|
||||
label: string,
|
||||
type: string,
|
||||
placeholder: string,
|
||||
required = true,
|
||||
) => (
|
||||
<div>
|
||||
<label htmlFor={id} className="mb-2 block text-sm font-medium">
|
||||
{label}{" "}
|
||||
{!isEditMode && required && <span className="text-text-error">*</span>}
|
||||
{isEditMode && (
|
||||
<span className="text-text-neutral-tertiary text-xs">
|
||||
(leave empty to keep existing)
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
id={id}
|
||||
type={type}
|
||||
value={formData[id] || ""}
|
||||
onChange={(e) => handleFieldChange(id, e.target.value)}
|
||||
placeholder={
|
||||
isEditMode ? `Enter new ${label} or leave empty` : placeholder
|
||||
}
|
||||
className="border-border-neutral-primary bg-bg-neutral-primary w-full rounded-lg border px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!providerConfig) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
@@ -184,8 +301,29 @@ export const ConnectLLMProvider = ({
|
||||
}
|
||||
|
||||
const mainFields = getMainFields(provider);
|
||||
const isFormValid = isProviderFormValid(provider, formData, isEditMode);
|
||||
const isLoading = status !== "idle";
|
||||
|
||||
const isBedrockProvider = provider === "bedrock";
|
||||
|
||||
const isBedrockFormValid = (): boolean => {
|
||||
if (isEditMode) return true;
|
||||
|
||||
const hasRegion = !!formData.region?.trim();
|
||||
|
||||
if (bedrockMode === BEDROCK_CREDENTIAL_MODES.API_KEY) {
|
||||
return !!formData.api_key?.trim() && hasRegion;
|
||||
}
|
||||
|
||||
return (
|
||||
!!formData.access_key_id?.trim() &&
|
||||
!!formData.secret_access_key?.trim() &&
|
||||
hasRegion
|
||||
);
|
||||
};
|
||||
|
||||
const isFormValid = isBedrockProvider
|
||||
? isBedrockFormValid()
|
||||
: isProviderFormValid(provider, formData, isEditMode);
|
||||
const isLoading = status !== CONNECTION_STATUS.IDLE;
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
@@ -209,40 +347,92 @@ export const ConnectLLMProvider = ({
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
{mainFields.map((field) => (
|
||||
<div key={field.name}>
|
||||
<label
|
||||
htmlFor={field.name}
|
||||
className="mb-2 block text-sm font-medium"
|
||||
>
|
||||
{field.label}{" "}
|
||||
{!isEditMode && field.required && (
|
||||
<span className="text-text-error">*</span>
|
||||
)}
|
||||
{isEditMode && (
|
||||
<span className="text-text-neutral-tertiary text-xs">
|
||||
(leave empty to keep existing)
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
id={field.name}
|
||||
type={field.type}
|
||||
value={formData[field.name] || ""}
|
||||
onChange={(e) => handleFieldChange(field.name, e.target.value)}
|
||||
placeholder={
|
||||
isEditMode
|
||||
? `Enter new ${field.label.toLowerCase()} or leave empty`
|
||||
: field.placeholder
|
||||
}
|
||||
className="border-border-neutral-primary bg-bg-neutral-primary w-full rounded-lg border px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{isBedrockProvider ? (
|
||||
<>
|
||||
{bedrockMode === BEDROCK_CREDENTIAL_MODES.API_KEY && (
|
||||
<div className="border-border-warning-primary bg-bg-warning-secondary rounded-lg border p-4">
|
||||
<p className="text-text-warning text-sm font-medium">
|
||||
Recommended only for exploration of Amazon Bedrock.
|
||||
</p>
|
||||
<p className="text-text-warning mt-1 text-xs">
|
||||
Please ensure you're using long-term Bedrock API keys.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{bedrockMode === BEDROCK_CREDENTIAL_MODES.API_KEY ? (
|
||||
<>
|
||||
{renderFormField(
|
||||
"api_key",
|
||||
"API key (long-term)",
|
||||
"password",
|
||||
"Enter your long-term API key",
|
||||
)}
|
||||
{renderFormField(
|
||||
"region",
|
||||
"AWS region",
|
||||
"text",
|
||||
"Enter the AWS region",
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{renderFormField(
|
||||
"access_key_id",
|
||||
"AWS access key ID",
|
||||
"password",
|
||||
"Enter the AWS Access Key ID",
|
||||
)}
|
||||
{renderFormField(
|
||||
"secret_access_key",
|
||||
"AWS secret access key",
|
||||
"password",
|
||||
"Enter the AWS Secret Access Key",
|
||||
)}
|
||||
{renderFormField(
|
||||
"region",
|
||||
"AWS region",
|
||||
"text",
|
||||
"Enter the AWS Region",
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
mainFields.map((field) => (
|
||||
<div key={field.name}>
|
||||
<label
|
||||
htmlFor={field.name}
|
||||
className="mb-2 block text-sm font-medium"
|
||||
>
|
||||
{field.label}{" "}
|
||||
{!isEditMode && field.required && (
|
||||
<span className="text-text-error">*</span>
|
||||
)}
|
||||
{isEditMode && (
|
||||
<span className="text-text-neutral-tertiary text-xs">
|
||||
(leave empty to keep existing)
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
id={field.name}
|
||||
type={field.type}
|
||||
value={formData[field.name] || ""}
|
||||
onChange={(e) => handleFieldChange(field.name, e.target.value)}
|
||||
placeholder={
|
||||
isEditMode
|
||||
? `Enter new ${field.label} or leave empty`
|
||||
: field.placeholder
|
||||
}
|
||||
className="border-border-neutral-primary bg-bg-neutral-primary w-full rounded-lg border px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
<FormButtons
|
||||
onCancel={() => router.push("/lighthouse/config")}
|
||||
submitText={getSubmitText()}
|
||||
submitText={isLoading ? getLoadingText() : getSubmitText()}
|
||||
loadingText={getLoadingText()}
|
||||
isDisabled={!isFormValid || isLoading}
|
||||
/>
|
||||
|
||||
59
ui/components/lighthouse/select-bedrock-auth-method.tsx
Normal file
59
ui/components/lighthouse/select-bedrock-auth-method.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { RadioGroup } from "@heroui/radio";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { CustomRadio } from "@/components/ui/custom";
|
||||
|
||||
const BEDROCK_AUTH_METHODS = {
|
||||
API_KEY: "api_key",
|
||||
IAM: "iam",
|
||||
} as const;
|
||||
|
||||
type BedrockAuthMethod =
|
||||
(typeof BEDROCK_AUTH_METHODS)[keyof typeof BEDROCK_AUTH_METHODS];
|
||||
|
||||
export const SelectBedrockAuthMethod = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const currentAuth = searchParams.get("auth") as BedrockAuthMethod | null;
|
||||
|
||||
const handleSelectionChange = (value: string) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set("auth", value);
|
||||
router.push(`?${params.toString()}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<div>
|
||||
<h2 className="mb-2 text-xl font-semibold">Connect Amazon Bedrock</h2>
|
||||
<p className="text-text-neutral-secondary text-sm">
|
||||
Choose how you want to authenticate with Amazon Bedrock. You can use a
|
||||
dedicated Bedrock API key or long-term AWS access keys.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<RadioGroup
|
||||
className="flex flex-col gap-3"
|
||||
value={currentAuth || ""}
|
||||
onValueChange={handleSelectionChange}
|
||||
>
|
||||
<CustomRadio value={BEDROCK_AUTH_METHODS.API_KEY}>
|
||||
<div className="flex items-center">
|
||||
<span className="ml-2">Use Bedrock API Key</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
|
||||
<CustomRadio value={BEDROCK_AUTH_METHODS.IAM}>
|
||||
<div className="flex items-center">
|
||||
<span className="ml-2">Use AWS Access Key & Secret</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user