feature: aws secrets manager sync

This commit is contained in:
Scott Wilson
2025-02-04 19:58:30 -08:00
parent 4f874734ab
commit 3f02481e78
54 changed files with 1051 additions and 40 deletions
@@ -1,30 +1,11 @@
import { Controller, useFormContext } from "react-hook-form";
import { components, OptionProps, SingleValue } from "react-select";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { Badge, FilterableSelect, FormControl, Input } from "@app/components/v2";
import { AWS_REGIONS } from "@app/helpers/appConnections";
import { FormControl, Input } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
const Option = ({ isSelected, children, ...props }: OptionProps<(typeof AWS_REGIONS)[number]>) => {
return (
<components.Option isSelected={isSelected} {...props}>
<div className="flex flex-row items-center justify-between">
<p className="truncate">{children}</p>
<Badge variant="success" className="ml-1 mr-auto cursor-pointer">
{props.data.slug}
</Badge>
{isSelected && (
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
)}
</div>
</components.Option>
);
};
import { AwsRegionSelect } from "./shared";
export const AwsParameterStoreSyncFields = () => {
const { control } = useFormContext<
@@ -37,17 +18,7 @@ export const AwsParameterStoreSyncFields = () => {
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Region">
<FilterableSelect
value={AWS_REGIONS.find((region) => region.slug === value)}
onChange={(option) =>
onChange((option as SingleValue<(typeof AWS_REGIONS)[number]>)?.slug)
}
options={AWS_REGIONS}
placeholder="Select region..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
components={{ Option }}
/>
<AwsRegionSelect value={value} onChange={onChange} />
</FormControl>
)}
control={control}
@@ -0,0 +1,96 @@
import { Controller, useFormContext } from "react-hook-form";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FormControl, Input, Select, SelectItem } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
import { TSecretSyncForm } from "../schemas";
import { AwsRegionSelect } from "./shared";
export const AwsSecretsManagerSyncFields = () => {
const { control, watch } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
>();
const mappingBehavior = watch("destinationConfig.mappingBehavior");
return (
<>
<SecretSyncConnectionField />
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Region">
<AwsRegionSelect value={value} onChange={onChange} />
</FormControl>
)}
control={control}
name="destinationConfig.region"
/>
<Controller
name="destinationConfig.mappingBehavior"
control={control}
defaultValue={AwsSecretsManagerSyncMappingBehavior.OneToOne}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipClassName="max-w-lg py-3"
tooltipText={
<div className="flex flex-col gap-3">
<p>Specify how Infisical should map secrets to AWS Secrets Manager:</p>
<ul className="flex list-disc flex-col gap-3 pl-4">
<li>
<p className="text-mineshaft-300">
<span className="font-medium text-bunker-200">One-To-One</span>: Each
Infisical secret will be mapped to a separate AWS Secrets Manager secret.
</p>
</li>
<li>
<p className="text-mineshaft-300">
<span className="font-medium text-bunker-200">Many-To-One</span>: All
Infisical secrets will be mapped to a single AWS Secrets Manager secret.
</p>
</li>
</ul>
</div>
}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Mapping Behavior"
>
<Select
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
placeholder="Select an option..."
dropdownContainerClassName="max-w-none"
>
{Object.values(AwsSecretsManagerSyncMappingBehavior).map((behavior) => {
return (
<SelectItem className="capitalize" value={behavior} key={behavior}>
{behavior}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
{mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne && (
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="AWS Secrets Manager Secret Name"
>
<Input value={value} onChange={onChange} placeholder="Secret name..." />
</FormControl>
)}
control={control}
name="destinationConfig.secretName"
/>
)}
</>
);
};
@@ -4,6 +4,7 @@ import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
import { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields";
import { AwsSecretsManagerSyncFields } from "./AwsSecretsManagerSyncFields";
import { GcpSyncFields } from "./GcpSyncFields";
import { GitHubSyncFields } from "./GitHubSyncFields";
@@ -15,6 +16,8 @@ export const SecretSyncDestinationFields = () => {
switch (destination) {
case SecretSync.AWSParameterStore:
return <AwsParameterStoreSyncFields />;
case SecretSync.AWSSecretsManager:
return <AwsSecretsManagerSyncFields />;
case SecretSync.GitHub:
return <GitHubSyncFields />;
case SecretSync.GCPSecretManager:
@@ -0,0 +1,41 @@
import { components, OptionProps, SingleValue } from "react-select";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, FilterableSelect } from "@app/components/v2";
import { AWS_REGIONS } from "@app/helpers/appConnections";
const Option = ({ isSelected, children, ...props }: OptionProps<(typeof AWS_REGIONS)[number]>) => {
return (
<components.Option isSelected={isSelected} {...props}>
<div className="flex flex-row items-center justify-between">
<p className="truncate">{children}</p>
<Badge variant="success" className="ml-1 mr-auto cursor-pointer">
{props.data.slug}
</Badge>
{isSelected && (
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
)}
</div>
</components.Option>
);
};
type Props = {
value: string;
onChange: (value: string | undefined) => void;
};
export const AwsRegionSelect = ({ value, onChange }: Props) => {
return (
<FilterableSelect
value={AWS_REGIONS.find((region) => region.slug === value)}
onChange={(option) => onChange((option as SingleValue<(typeof AWS_REGIONS)[number]>)?.slug)}
options={AWS_REGIONS}
placeholder="Select region..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
components={{ Option }}
/>
);
};
@@ -0,0 +1 @@
export * from "./AwsRegionSelect";
@@ -0,0 +1,39 @@
import { useFormContext } from "react-hook-form";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { Badge } from "@app/components/v2";
import { AWS_REGIONS } from "@app/helpers/appConnections";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
export const AwsSecretsManagerSyncReviewFields = () => {
const { watch } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
>();
const [region, mappingBehavior, secretName] = watch([
"destinationConfig.region",
"destinationConfig.mappingBehavior",
"destinationConfig.secretName"
]);
const awsRegion = AWS_REGIONS.find((r) => r.slug === region);
return (
<>
<SecretSyncLabel label="Region">
{awsRegion?.name}
<Badge className="ml-1" variant="success">
{awsRegion?.slug}{" "}
</Badge>
</SecretSyncLabel>
<SecretSyncLabel className="capitalize" label="Mapping Behavior">
{mappingBehavior}
</SecretSyncLabel>
{mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne && (
<SecretSyncLabel label="Secret Name">{secretName}</SecretSyncLabel>
)}
</>
);
};
@@ -3,6 +3,7 @@ import { useFormContext } from "react-hook-form";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { AwsSecretsManagerSyncReviewFields } from "@app/components/secret-syncs/forms/SecretSyncReviewFields/AwsSecretsManagerSyncReviewFields";
import { Badge } from "@app/components/v2";
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { SecretSync } from "@app/hooks/api/secretSyncs";
@@ -36,6 +37,9 @@ export const SecretSyncReviewFields = () => {
case SecretSync.AWSParameterStore:
DestinationFieldsComponent = <AwsParameterStoreSyncReviewFields />;
break;
case SecretSync.AWSSecretsManager:
DestinationFieldsComponent = <AwsSecretsManagerSyncReviewFields />;
break;
case SecretSync.GitHub:
DestinationFieldsComponent = <GitHubSyncReviewFields />;
break;
@@ -0,0 +1,30 @@
import { z } from "zod";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
export const AwsSecretsManagerSyncDestinationSchema = z.object({
destination: z.literal(SecretSync.AWSSecretsManager),
destinationConfig: z
.discriminatedUnion("mappingBehavior", [
z.object({
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.OneToOne)
}),
z.object({
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne),
secretName: z
.string()
.regex(
/^[a-zA-Z0-9/_+=.@-]+$/,
"Secret name must contain only alphanumeric characters and the characters /_+=.@-"
)
.min(1, "Secret name is required")
.max(256, "Secret name cannot exceed 256 characters")
})
])
.and(
z.object({
region: z.string().min(1, "Region required")
})
)
});
@@ -1,5 +1,6 @@
import { z } from "zod";
import { AwsSecretsManagerSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema";
import { GitHubSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/github-sync-destination-schema";
import { SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs";
import { slugSchema } from "@app/lib/schemas";
@@ -32,6 +33,7 @@ const BaseSecretSyncSchema = z.object({
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncDestinationSchema,
AwsSecretsManagerSyncDestinationSchema,
GitHubSyncDestinationSchema,
GcpSyncDestinationSchema
]);