mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-04-01 13:47:21 +00:00
Compare commits
1 Commits
wanr-sensi
...
nitpicks/7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5314d84b3 |
@@ -2,6 +2,7 @@
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
import {
|
||||
apiBaseUrl,
|
||||
@@ -307,3 +308,223 @@ export const deleteProvider = async (formData: FormData) => {
|
||||
return { error: getErrorMessage(error) };
|
||||
}
|
||||
};
|
||||
|
||||
// Bulk provider import functionality
|
||||
export const bulkImportProviders = async (formData: FormData) => {
|
||||
const headers = await getAuthHeaders({ contentType: true });
|
||||
const yamlContent = formData.get("yamlContent") as string;
|
||||
|
||||
if (!yamlContent) {
|
||||
return { error: "YAML content is required" };
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const providers = yaml.load(yamlContent);
|
||||
|
||||
if (!Array.isArray(providers)) {
|
||||
return { error: "YAML content must be an array of provider configurations" };
|
||||
}
|
||||
|
||||
const results = [];
|
||||
const errors = [];
|
||||
|
||||
for (let i = 0; i < providers.length; i++) {
|
||||
const provider = providers[i];
|
||||
const providerNum = i + 1;
|
||||
|
||||
try {
|
||||
// Step 1: Create provider
|
||||
const providerResponse = await fetch(`${apiBaseUrl}/providers`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
type: "providers",
|
||||
attributes: {
|
||||
provider: provider.provider,
|
||||
uid: provider.uid,
|
||||
...(provider.alias && { alias: provider.alias }),
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const providerData = await providerResponse.json();
|
||||
|
||||
if (!providerResponse.ok) {
|
||||
errors.push({
|
||||
provider: providerNum,
|
||||
step: "provider_creation",
|
||||
error: providerData,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const providerId = providerData.data.id;
|
||||
results.push({
|
||||
provider: providerNum,
|
||||
providerId,
|
||||
providerData,
|
||||
});
|
||||
|
||||
// Step 2: Create credentials if provided
|
||||
if (provider.auth_method && provider.credentials) {
|
||||
try {
|
||||
const { secretType, secret } = buildCredentialsFromYaml(provider);
|
||||
|
||||
const secretResponse = await fetch(`${apiBaseUrl}/providers/secrets`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
type: "provider-secrets",
|
||||
attributes: {
|
||||
secret_type: secretType,
|
||||
secret,
|
||||
name: provider.alias || `${provider.provider}-${provider.uid}`,
|
||||
},
|
||||
relationships: {
|
||||
provider: {
|
||||
data: { id: providerId, type: "providers" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const secretData = await secretResponse.json();
|
||||
|
||||
if (!secretResponse.ok) {
|
||||
errors.push({
|
||||
provider: providerNum,
|
||||
step: "credentials_creation",
|
||||
error: secretData,
|
||||
});
|
||||
} else {
|
||||
results[results.length - 1].secretData = secretData;
|
||||
}
|
||||
} catch (credError) {
|
||||
errors.push({
|
||||
provider: providerNum,
|
||||
step: "credentials_processing",
|
||||
error: getErrorMessage(credError),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push({
|
||||
provider: providerNum,
|
||||
step: "general",
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
revalidatePath("/providers");
|
||||
return parseStringify({
|
||||
success: true,
|
||||
results,
|
||||
errors,
|
||||
summary: {
|
||||
total: providers.length,
|
||||
successful: results.length,
|
||||
failed: errors.length,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
error: getErrorMessage(error),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to build credentials from YAML provider config
|
||||
function buildCredentialsFromYaml(provider: any) {
|
||||
const { provider: providerType, auth_method: authMethod, credentials } = provider;
|
||||
|
||||
let secretType = "static";
|
||||
let secret: any = {};
|
||||
|
||||
if (providerType === "aws") {
|
||||
if (authMethod === "role") {
|
||||
secretType = "role";
|
||||
secret = {
|
||||
role_arn: credentials.role_arn,
|
||||
external_id: credentials.external_id,
|
||||
...(credentials.session_name && { role_session_name: credentials.session_name }),
|
||||
...(credentials.duration_seconds && { session_duration: credentials.duration_seconds }),
|
||||
...(credentials.access_key_id && { aws_access_key_id: credentials.access_key_id }),
|
||||
...(credentials.secret_access_key && { aws_secret_access_key: credentials.secret_access_key }),
|
||||
...(credentials.session_token && { aws_session_token: credentials.session_token }),
|
||||
};
|
||||
} else if (authMethod === "credentials") {
|
||||
secretType = "static";
|
||||
secret = {
|
||||
aws_access_key_id: credentials.access_key_id,
|
||||
aws_secret_access_key: credentials.secret_access_key,
|
||||
...(credentials.session_token && { aws_session_token: credentials.session_token }),
|
||||
};
|
||||
}
|
||||
} else if (providerType === "azure") {
|
||||
if (authMethod === "service_principal") {
|
||||
secretType = "static";
|
||||
secret = {
|
||||
tenant_id: credentials.tenant_id,
|
||||
client_id: credentials.client_id,
|
||||
client_secret: credentials.client_secret,
|
||||
};
|
||||
}
|
||||
} else if (providerType === "gcp") {
|
||||
if (authMethod === "service_account" || authMethod === "service_account_json") {
|
||||
secretType = "service_account";
|
||||
if (credentials.inline_json) {
|
||||
secret = { service_account_key: credentials.inline_json };
|
||||
} else if (credentials.service_account_key_json_path) {
|
||||
// For file paths, we can't read the file in the browser
|
||||
throw new Error("File path credentials are not supported in bulk import. Use inline_json instead.");
|
||||
}
|
||||
} else if (authMethod === "oauth2" || authMethod === "adc") {
|
||||
secretType = "static";
|
||||
secret = {
|
||||
client_id: credentials.client_id,
|
||||
client_secret: credentials.client_secret,
|
||||
refresh_token: credentials.refresh_token,
|
||||
};
|
||||
}
|
||||
} else if (providerType === "kubernetes") {
|
||||
if (authMethod === "kubeconfig") {
|
||||
secretType = "static";
|
||||
secret = {
|
||||
kubeconfig_content: credentials.kubeconfig_inline || credentials.kubeconfig_content,
|
||||
};
|
||||
}
|
||||
} else if (providerType === "m365") {
|
||||
if (authMethod === "service_principal") {
|
||||
secretType = "static";
|
||||
secret = {
|
||||
tenant_id: credentials.tenant_id,
|
||||
client_id: credentials.client_id,
|
||||
client_secret: credentials.client_secret,
|
||||
...(credentials.username && { user: credentials.username }),
|
||||
...(credentials.password && { password: credentials.password }),
|
||||
};
|
||||
}
|
||||
} else if (providerType === "github") {
|
||||
if (authMethod === "personal_access_token") {
|
||||
secretType = "static";
|
||||
secret = { personal_access_token: credentials.token };
|
||||
} else if (authMethod === "oauth_app_token") {
|
||||
secretType = "static";
|
||||
secret = { oauth_app_token: credentials.oauth_token };
|
||||
} else if (authMethod === "github_app") {
|
||||
secretType = "static";
|
||||
secret = {
|
||||
github_app_id: parseInt(credentials.app_id, 10),
|
||||
github_app_key_content: credentials.private_key_inline || credentials.private_key,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { secretType, secret };
|
||||
}
|
||||
|
||||
@@ -1,19 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronDownIcon, FileTextIcon, PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { AddIcon } from "../icons";
|
||||
import { CustomButton } from "../ui/custom";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
import { BulkImportModal } from "./bulk-import";
|
||||
|
||||
export const AddProviderButton = () => {
|
||||
const [isBulkImportOpen, setIsBulkImportOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<CustomButton
|
||||
asLink="/providers/connect-account"
|
||||
ariaLabel="Add Cloud Provider"
|
||||
variant="solid"
|
||||
color="action"
|
||||
size="md"
|
||||
endContent={<AddIcon size={20} />}
|
||||
>
|
||||
Add Cloud Provider
|
||||
</CustomButton>
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<CustomButton
|
||||
ariaLabel="Add Cloud Provider"
|
||||
variant="solid"
|
||||
color="action"
|
||||
size="md"
|
||||
endContent={
|
||||
<div className="flex items-center gap-1">
|
||||
<AddIcon size={20} />
|
||||
<ChevronDownIcon size={16} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Add Cloud Provider
|
||||
</CustomButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuItem asChild>
|
||||
<a
|
||||
href="/providers/connect-account"
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
>
|
||||
<PlusIcon size={16} />
|
||||
Add Single Provider
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => setIsBulkImportOpen(true)}
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
>
|
||||
<FileTextIcon size={16} />
|
||||
Bulk Import from YAML
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<BulkImportModal
|
||||
isOpen={isBulkImportOpen}
|
||||
onClose={() => setIsBulkImportOpen(false)}
|
||||
onSuccess={() => {
|
||||
// Refresh the page to show new providers
|
||||
window.location.reload();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
196
ui/components/providers/bulk-import/bulk-import-modal.tsx
Normal file
196
ui/components/providers/bulk-import/bulk-import-modal.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
"use client";
|
||||
|
||||
import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from "@nextui-org/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
import { bulkImportProviders } from "@/actions/providers/providers";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton, CustomTextarea } from "@/components/ui/custom";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { bulkProviderImportFormSchema } from "@/types";
|
||||
|
||||
export type BulkImportFormValues = z.infer<typeof bulkProviderImportFormSchema>;
|
||||
|
||||
interface BulkImportModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export const BulkImportModal = ({ isOpen, onClose, onSuccess }: BulkImportModalProps) => {
|
||||
const { toast } = useToast();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const form = useForm<BulkImportFormValues>({
|
||||
resolver: zodResolver(bulkProviderImportFormSchema),
|
||||
defaultValues: {
|
||||
yamlContent: "",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (values: BulkImportFormValues) => {
|
||||
setIsSubmitting(true);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("yamlContent", values.yamlContent);
|
||||
|
||||
try {
|
||||
const result = await bulkImportProviders(formData);
|
||||
|
||||
if (result?.error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Import Failed",
|
||||
description: result.error,
|
||||
});
|
||||
} else if (result?.success) {
|
||||
const { summary, errors } = result;
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Partial Import Success",
|
||||
description: `${summary.successful} of ${summary.total} providers imported successfully. ${errors.length} failed.`,
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "Import Successful",
|
||||
description: `Successfully imported ${summary.successful} provider${summary.successful === 1 ? '' : 's'}.`,
|
||||
});
|
||||
}
|
||||
|
||||
form.reset();
|
||||
onClose();
|
||||
onSuccess?.();
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Import Error",
|
||||
description: error instanceof Error ? error.message : "An unexpected error occurred",
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isSubmitting) {
|
||||
form.reset();
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const exampleYaml = `# Example YAML configuration for bulk provider import
|
||||
- provider: aws
|
||||
uid: "123456789012"
|
||||
alias: "production-account"
|
||||
auth_method: role
|
||||
credentials:
|
||||
role_arn: "arn:aws:iam::123456789012:role/ProwlerScanRole"
|
||||
external_id: "prowler-external-id"
|
||||
|
||||
- provider: azure
|
||||
uid: "00000000-1111-2222-3333-444444444444"
|
||||
alias: "azure-production"
|
||||
auth_method: service_principal
|
||||
credentials:
|
||||
tenant_id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
||||
client_id: "ffffffff-1111-2222-3333-444444444444"
|
||||
client_secret: "your-client-secret"
|
||||
|
||||
- provider: gcp
|
||||
uid: "my-gcp-project"
|
||||
alias: "gcp-production"
|
||||
auth_method: service_account
|
||||
credentials:
|
||||
inline_json:
|
||||
type: "service_account"
|
||||
project_id: "my-gcp-project"
|
||||
private_key_id: "key-id"
|
||||
private_key: "-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"
|
||||
client_email: "service-account@project.iam.gserviceaccount.com"
|
||||
client_id: "123456789"
|
||||
auth_uri: "https://accounts.google.com/o/oauth2/auth"
|
||||
token_uri: "https://oauth2.googleapis.com/token"`;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(open) => !open && handleClose()}
|
||||
size="4xl"
|
||||
classNames={{
|
||||
base: "dark:bg-prowler-blue-800",
|
||||
closeButton: "rounded-md",
|
||||
}}
|
||||
backdrop="blur"
|
||||
placement="center"
|
||||
scrollBehavior="inside"
|
||||
>
|
||||
<ModalContent className="py-4">
|
||||
{(_onClose) => (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<ModalHeader className="flex flex-col gap-1 py-0">
|
||||
<h2 className="text-xl font-semibold">Bulk Import Providers</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Import multiple cloud providers at once using YAML configuration
|
||||
</p>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="space-y-4">
|
||||
<CustomTextarea
|
||||
control={form.control}
|
||||
name="yamlContent"
|
||||
label="YAML Configuration"
|
||||
placeholder={exampleYaml}
|
||||
variant="bordered"
|
||||
minRows={15}
|
||||
maxRows={25}
|
||||
isRequired
|
||||
isInvalid={!!form.formState.errors.yamlContent}
|
||||
description="Paste your YAML configuration here. Each provider entry should include provider type, UID, alias, and credentials."
|
||||
/>
|
||||
|
||||
<div className="bg-gray-50 dark:bg-prowler-blue-700 p-4 rounded-lg">
|
||||
<h3 className="font-medium text-sm mb-2">Supported Provider Types:</h3>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<div>• aws (Account ID)</div>
|
||||
<div>• azure (Subscription ID)</div>
|
||||
<div>• gcp (Project ID)</div>
|
||||
<div>• kubernetes (Context)</div>
|
||||
<div>• m365 (Domain ID)</div>
|
||||
<div>• github (Username)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<CustomButton
|
||||
type="button"
|
||||
variant="faded"
|
||||
onPress={handleClose}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</CustomButton>
|
||||
<CustomButton
|
||||
type="submit"
|
||||
variant="solid"
|
||||
color="action"
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Importing..." : "Import Providers"}
|
||||
</CustomButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
1
ui/components/providers/bulk-import/index.ts
Normal file
1
ui/components/providers/bulk-import/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { BulkImportModal } from "./bulk-import-modal";
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "../../store/ui/store-initializer";
|
||||
export * from "./add-provider-button";
|
||||
export * from "./bulk-import";
|
||||
export * from "./credentials-update-info";
|
||||
export * from "./forms/delete-form";
|
||||
export * from "./link-to-scans";
|
||||
|
||||
@@ -148,6 +148,93 @@ export const parseYamlValidation = (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates if a YAML string contains valid bulk provider configurations
|
||||
*/
|
||||
export const validateBulkProviderYaml = (
|
||||
val: string,
|
||||
): { isValid: boolean; error?: string; providers?: any[] } => {
|
||||
try {
|
||||
const parsed = yaml.load(val);
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: "YAML content must be an array of provider configurations"
|
||||
};
|
||||
}
|
||||
|
||||
if (parsed.length === 0) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: "At least one provider configuration is required"
|
||||
};
|
||||
}
|
||||
|
||||
// Validate each provider entry
|
||||
for (let i = 0; i < parsed.length; i++) {
|
||||
const item = parsed[i];
|
||||
const providerNum = i + 1;
|
||||
|
||||
if (!item || typeof item !== 'object') {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `Provider ${providerNum}: Must be an object`
|
||||
};
|
||||
}
|
||||
|
||||
if (!item.provider || typeof item.provider !== 'string') {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `Provider ${providerNum}: 'provider' field is required and must be a string`
|
||||
};
|
||||
}
|
||||
|
||||
const validProviders = ['aws', 'azure', 'gcp', 'kubernetes', 'm365', 'github'];
|
||||
if (!validProviders.includes(item.provider)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `Provider ${providerNum}: Invalid provider type '${item.provider}'. Must be one of: ${validProviders.join(', ')}`
|
||||
};
|
||||
}
|
||||
|
||||
if (!item.uid || typeof item.uid !== 'string') {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `Provider ${providerNum}: 'uid' field is required and must be a string`
|
||||
};
|
||||
}
|
||||
|
||||
if (item.alias && typeof item.alias !== 'string') {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `Provider ${providerNum}: 'alias' field must be a string`
|
||||
};
|
||||
}
|
||||
|
||||
if (item.auth_method && typeof item.auth_method !== 'string') {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `Provider ${providerNum}: 'auth_method' field must be a string`
|
||||
};
|
||||
}
|
||||
|
||||
if (item.credentials && typeof item.credentials !== 'object') {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `Provider ${providerNum}: 'credentials' field must be an object`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true, providers: parsed };
|
||||
} catch (error: unknown) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown YAML parsing error";
|
||||
return { isValid: false, error: errorMessage };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a configuration (string or object) to YAML format
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
import { validateMutelistYaml, validateYaml } from "@/lib/yaml";
|
||||
@@ -409,3 +410,96 @@ export const mutedFindingsConfigFormSchema = z.object({
|
||||
}),
|
||||
id: z.string().optional(),
|
||||
});
|
||||
|
||||
export const bulkProviderImportFormSchema = z.object({
|
||||
yamlContent: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "YAML content is required" })
|
||||
.superRefine((val, ctx) => {
|
||||
const yamlValidation = validateYaml(val);
|
||||
if (!yamlValidation.isValid) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Invalid YAML format: ${yamlValidation.error}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = yaml.load(val);
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "YAML content must be an array of provider configurations",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed.length === 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "At least one provider configuration is required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate each provider entry
|
||||
parsed.forEach((item: any, index: number) => {
|
||||
if (!item || typeof item !== 'object') {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Provider ${index + 1}: Must be an object`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.provider || typeof item.provider !== 'string') {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Provider ${index + 1}: 'provider' field is required and must be a string`,
|
||||
});
|
||||
} else if (!PROVIDER_TYPES.includes(item.provider as ProviderType)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Provider ${index + 1}: Invalid provider type '${item.provider}'. Must be one of: ${PROVIDER_TYPES.join(', ')}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!item.uid || typeof item.uid !== 'string') {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Provider ${index + 1}: 'uid' field is required and must be a string`,
|
||||
});
|
||||
}
|
||||
|
||||
if (item.alias && typeof item.alias !== 'string') {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Provider ${index + 1}: 'alias' field must be a string`,
|
||||
});
|
||||
}
|
||||
|
||||
if (item.auth_method && typeof item.auth_method !== 'string') {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Provider ${index + 1}: 'auth_method' field must be a string`,
|
||||
});
|
||||
}
|
||||
|
||||
if (item.credentials && typeof item.credentials !== 'object') {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Provider ${index + 1}: 'credentials' field must be an object`,
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Error parsing YAML: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user