feature: kms key, tags, and sync secret metadata support for aws secrets manager

This commit is contained in:
Scott Wilson
2025-02-19 20:38:18 -08:00
parent 9cdb4dcde9
commit f8ab2bcdfd
19 changed files with 672 additions and 44 deletions
@@ -214,7 +214,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
errorText={error?.message}
>
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/50"
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
id="auto-sync-enabled"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
@@ -9,7 +9,7 @@ import { TSecretSyncForm } from "../schemas";
import { AwsRegionSelect } from "./shared";
export const AwsSecretsManagerSyncFields = () => {
const { control, watch } = useFormContext<
const { control, watch, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
>();
@@ -59,7 +59,10 @@ export const AwsSecretsManagerSyncFields = () => {
>
<Select
value={value}
onValueChange={(val) => onChange(val)}
onValueChange={(val) => {
onChange(val);
setValue("syncOptions.syncSecretMetadataAsTags", false);
}}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
placeholder="Select an option..."
@@ -0,0 +1,208 @@
import { Fragment } from "react";
import { Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form";
import { SingleValue } from "react-select";
import { faPlus, faQuestionCircle, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
FilterableSelect,
FormControl,
FormLabel,
IconButton,
Input,
Switch,
Tooltip
} from "@app/components/v2";
import {
TAwsConnectionKmsKey,
useListAwsConnectionKmsKeys
} from "@app/hooks/api/appConnections/aws";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
import { TSecretSyncForm } from "../schemas";
export const AwsSecretsManagerSyncOptionsFields = () => {
const { control, watch } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
>();
const region = watch("destinationConfig.region");
const connectionId = useWatch({ name: "connection.id", control });
const mappingBehavior = watch("destinationConfig.mappingBehavior");
const { data: kmsKeys = [], isPending: isKmsKeysPending } = useListAwsConnectionKmsKeys(
{
connectionId,
region,
destination: SecretSync.AWSSecretsManager
},
{ enabled: Boolean(connectionId && region) }
);
const tagFields = useFieldArray({
control,
name: "syncOptions.tags"
});
return (
<>
<Controller
name="syncOptions.keyId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText="The AWS KMS key to encrypt secrets with"
isError={Boolean(error)}
errorText={error?.message}
label="KMS Key"
>
<FilterableSelect
isLoading={isKmsKeysPending && Boolean(connectionId && region)}
isDisabled={!connectionId}
value={kmsKeys.find((org) => org.alias === value) ?? null}
onChange={(option) =>
onChange((option as SingleValue<TAwsConnectionKmsKey>)?.alias ?? null)
}
// eslint-disable-next-line react/no-unstable-nested-components
noOptionsMessage={({ inputValue }) =>
inputValue ? undefined : (
<p>
To configure a KMS key, ensure the following permissions are present on the
selected IAM role:{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:ListKeys&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:ListAliases&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:Encrypt&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:Decrypt&#34;
</span>
.
</p>
)
}
options={kmsKeys}
placeholder="Leave blank to use default KMS key"
getOptionLabel={(option) =>
option.alias === "alias/aws/secretsmanager"
? `${option.alias} (Default)`
: option.alias
}
getOptionValue={(option) => option.alias}
/>
</FormControl>
)}
/>
<FormLabel label="Tags" tooltipText="Add tags to secrets synced by Infisical" />
<div className="mb-3 grid max-h-[20vh] grid-cols-12 flex-col items-end gap-2 overflow-y-auto">
{tagFields.fields.map(({ id: tagFieldId }, i) => (
<Fragment key={tagFieldId}>
<div className="col-span-5">
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
<Controller
control={control}
name={`syncOptions.tags.${i}.key`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input className="text-xs" {...field} />
</FormControl>
)}
/>
</div>
<div className="col-span-6">
{i === 0 && (
<FormLabel label="Value" className="text-xs text-mineshaft-400" isOptional />
)}
<Controller
control={control}
name={`syncOptions.tags.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input className="text-xs" {...field} />
</FormControl>
)}
/>
</div>
<Tooltip content="Remove tag" position="right">
<IconButton
variant="plain"
ariaLabel="Remove tag"
className="col-span-1 mb-1.5"
colorSchema="danger"
size="xs"
onClick={() => tagFields.remove(i)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
</Fragment>
))}
</div>
<div className="mb-6 mt-2 flex">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => tagFields.append({ key: "", value: "" })}
>
Add Tag
</Button>
</div>
{mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne && (
<Controller
name="syncOptions.syncSecretMetadataAsTags"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error?.message)} errorText={error?.message}>
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="overwrite-existing-secrets"
thumbClassName="bg-mineshaft-800"
isChecked={value}
onCheckedChange={onChange}
>
<p className="w-[14rem]">
Sync Secret Metadata as Tags{" "}
<Tooltip
className="max-w-md"
content={
<>
<p>
If enabled, metadata attached to secrets will be added as tags to secrets
synced by Infisical.
</p>
<p className="mt-4">
Manually configured tags from the field above will take precedence over
secret metadata when tag keys conflict.
</p>
</>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</p>
</Switch>
</FormControl>
)}
/>
)}
</>
);
};
@@ -9,6 +9,7 @@ import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
import { AwsParameterStoreSyncOptionsFields } from "./AwsParameterStoreSyncOptionsFields";
import { AwsSecretsManagerSyncOptionsFields } from "./AwsSecretsManagerSyncOptionsFields";
type Props = {
hideInitialSync?: boolean;
@@ -30,6 +31,8 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
AdditionalSyncOptionsFieldsComponent = <AwsParameterStoreSyncOptionsFields />;
break;
case SecretSync.AWSSecretsManager:
AdditionalSyncOptionsFieldsComponent = <AwsSecretsManagerSyncOptionsFields />;
break;
case SecretSync.GitHub:
case SecretSync.GCPSecretManager:
case SecretSync.AzureKeyVault:
@@ -1,8 +1,10 @@
import { useFormContext } from "react-hook-form";
import { faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { Badge } from "@app/components/v2";
import { Badge, Table, TBody, Td, Th, THead, Tooltip, Tr } 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";
@@ -37,3 +39,55 @@ export const AwsSecretsManagerSyncReviewFields = () => {
</>
);
};
export const AwsSecretsManagerSyncOptionsReviewFields = () => {
const { watch } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
>();
const [{ keyId, tags, syncSecretMetadataAsTags }] = watch(["syncOptions"]);
return (
<>
{keyId && <SecretSyncLabel label="KMS Key">{keyId}</SecretSyncLabel>}
{tags && tags.length > 0 && (
<SecretSyncLabel label="Tags">
<Tooltip
side="right"
className="max-w-xl p-1"
content={
<Table>
<THead>
<Th className="whitespace-nowrap p-2">Key</Th>
<Th className="p-2">Value</Th>
</THead>
<TBody>
{tags.map((tag) => (
<Tr key={tag.key}>
<Td className="p-2">{tag.key}</Td>
<Td className="p-2">{tag.value}</Td>
</Tr>
))}
</TBody>
</Table>
}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>
{tags.length} Tag{tags.length > 1 ? "s" : ""}
</span>
</Badge>
</div>
</Tooltip>
</SecretSyncLabel>
)}
{syncSecretMetadataAsTags && (
<SecretSyncLabel label="Sync Secret Metadata as Resource Tags">
<Badge variant="success">Enabled</Badge>
</SecretSyncLabel>
)}
</>
);
};
@@ -11,7 +11,10 @@ import {
AwsParameterStoreDestinationReviewFields,
AwsParameterStoreSyncOptionsReviewFields
} from "./AwsParameterStoreSyncReviewFields";
import { AwsSecretsManagerSyncReviewFields } from "./AwsSecretsManagerSyncReviewFields";
import {
AwsSecretsManagerSyncOptionsReviewFields,
AwsSecretsManagerSyncReviewFields
} from "./AwsSecretsManagerSyncReviewFields";
import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields";
import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
@@ -47,6 +50,7 @@ export const SecretSyncReviewFields = () => {
break;
case SecretSync.AWSSecretsManager:
DestinationFieldsComponent = <AwsSecretsManagerSyncReviewFields />;
AdditionalSyncOptionsFieldsComponent = <AwsSecretsManagerSyncOptionsReviewFields />;
break;
case SecretSync.GitHub:
DestinationFieldsComponent = <GitHubSyncReviewFields />;
@@ -15,7 +15,7 @@ export const AwsParameterStoreSyncDestinationSchema = BaseSecretSyncSchema(
"Keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
)
.min(1, "Key required")
.max(128, "AWS tag name cannot exceed 128 characters"),
.max(128, "Tag key cannot exceed 128 characters"),
value: z
.string()
.regex(
@@ -4,7 +4,33 @@ import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
export const AwsSecretsManagerSyncDestinationSchema = BaseSecretSyncSchema().merge(
export const AwsSecretsManagerSyncDestinationSchema = BaseSecretSyncSchema(
z.object({
keyId: z.string().optional(),
tags: z
.object({
key: z
.string()
.regex(
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
"Keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
)
.min(1, "Key required")
.max(128, "Tag key cannot exceed 128 characters"),
value: z
.string()
.regex(
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
"Values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
)
.max(256, "Tag value cannot exceed 256 characters")
})
.array()
.max(50)
.optional(),
syncSecretMetadataAsTags: z.boolean().optional()
})
).merge(
z.object({
destination: z.literal(SecretSync.AWSSecretsManager),
destinationConfig: z
@@ -1,6 +1,6 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
import { RootSyncOptions, TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
export type TAwsSecretsManagerSync = TRootSecretSync & {
destination: SecretSync.AWSSecretsManager;
@@ -19,6 +19,11 @@ export type TAwsSecretsManagerSync = TRootSecretSync & {
name: string;
id: string;
};
syncOptions: RootSyncOptions & {
keyId?: string;
tags?: { key: string; value?: string }[];
syncSecretMetadataAsTags?: boolean;
};
};
export enum AwsSecretsManagerSyncMappingBehavior {
OneToOne = "one-to-one",
@@ -0,0 +1,60 @@
import { faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { Badge, Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
import { TAwsSecretsManagerSync } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
type Props = {
secretSync: TAwsSecretsManagerSync;
};
export const AwsSecretsManagerSyncOptionsSection = ({ secretSync }: Props) => {
const {
syncOptions: { keyId, tags, syncSecretMetadataAsTags }
} = secretSync;
return (
<>
{keyId && <SecretSyncLabel label="KMS Key">{keyId}</SecretSyncLabel>}
{tags && tags.length > 0 && (
<SecretSyncLabel label="Tags">
<Tooltip
side="right"
className="max-w-xl p-1"
content={
<Table>
<THead>
<Th className="whitespace-nowrap p-2">Key</Th>
<Th className="p-2">Value</Th>
</THead>
<TBody>
{tags.map((tag) => (
<Tr key={tag.key}>
<Td className="p-2">{tag.key}</Td>
<Td className="p-2">{tag.value}</Td>
</Tr>
))}
</TBody>
</Table>
}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>
{tags.length} Tag{tags.length > 1 ? "s" : ""}
</span>
</Badge>
</div>
</Tooltip>
</SecretSyncLabel>
)}
{syncSecretMetadataAsTags && (
<SecretSyncLabel label="Sync Secret Metadata as Tags">
<Badge variant="success">Enabled</Badge>
</SecretSyncLabel>
)}
</>
);
};
@@ -11,6 +11,7 @@ import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP } from "@app/helpers/secretSyncs"
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
import { AwsParameterStoreSyncOptionsSection } from "./AwsParameterStoreSyncOptionsSection";
import { AwsSecretsManagerSyncOptionsSection } from "./AwsSecretsManagerSyncOptionsSection";
type Props = {
secretSync: TSecretSync;
@@ -36,6 +37,10 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
);
break;
case SecretSync.AWSSecretsManager:
AdditionalSyncOptionsComponent = (
<AwsSecretsManagerSyncOptionsSection secretSync={secretSync} />
);
break;
case SecretSync.GitHub:
case SecretSync.GCPSecretManager:
case SecretSync.AzureKeyVault: