mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 19:44:52 +00:00
feature: kms key, tags, and sync secret metadata support for aws secrets manager
This commit is contained in:
@@ -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}
|
||||
|
||||
+5
-2
@@ -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..."
|
||||
|
||||
+208
@@ -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">
|
||||
"kms:ListKeys"
|
||||
</span>
|
||||
,{" "}
|
||||
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||
"kms:ListAliases"
|
||||
</span>
|
||||
,{" "}
|
||||
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||
"kms:Encrypt"
|
||||
</span>
|
||||
,{" "}
|
||||
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||
"kms:Decrypt"
|
||||
</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>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+3
@@ -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:
|
||||
|
||||
+55
-1
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+5
-1
@@ -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 />;
|
||||
|
||||
+1
-1
@@ -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(
|
||||
|
||||
+27
-1
@@ -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",
|
||||
|
||||
+60
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+5
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user