mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feature: kms key, tags, and sync secret metadata support for aws secrets manager
This commit is contained in:
@@ -1727,6 +1727,11 @@ export const SecretSyncs = {
|
||||
keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.",
|
||||
tags: "Optional resource tags to add to parameters synced by Infisical.",
|
||||
syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as resource tags to parameters synced by Infisical.`
|
||||
},
|
||||
AWS_SECRETS_MANAGER: {
|
||||
keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.",
|
||||
tags: "Optional tags to add to secrets synced by Infisical.",
|
||||
syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as tags to secrets synced by Infisical.`
|
||||
}
|
||||
},
|
||||
DESTINATION_CONFIG: {
|
||||
|
||||
@@ -38,7 +38,7 @@ const AwsParameterStoreSyncOptionsSchema = z.object({
|
||||
"Invalid resource tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.min(1, "Resource tag key required")
|
||||
.max(128, "Resource tag name cannot exceed 128 characters"),
|
||||
.max(128, "Resource tag key cannot exceed 128 characters"),
|
||||
value: z
|
||||
.string()
|
||||
.regex(
|
||||
@@ -50,7 +50,7 @@ const AwsParameterStoreSyncOptionsSchema = z.object({
|
||||
.array()
|
||||
.max(50)
|
||||
.refine((items) => new Set(items.map((item) => item.key)).size === items.length, {
|
||||
message: "AWS tag keys must be unique"
|
||||
message: "Resource tag keys must be unique"
|
||||
})
|
||||
.optional()
|
||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.tags),
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
import { UntagResourceCommandOutput } from "@aws-sdk/client-kms";
|
||||
import {
|
||||
BatchGetSecretValueCommand,
|
||||
CreateSecretCommand,
|
||||
CreateSecretCommandInput,
|
||||
DeleteSecretCommand,
|
||||
DeleteSecretResponse,
|
||||
DescribeSecretCommand,
|
||||
DescribeSecretCommandInput,
|
||||
ListSecretsCommand,
|
||||
SecretsManagerClient,
|
||||
TagResourceCommand,
|
||||
TagResourceCommandOutput,
|
||||
UntagResourceCommand,
|
||||
UpdateSecretCommand,
|
||||
UpdateSecretCommandInput
|
||||
} from "@aws-sdk/client-secrets-manager";
|
||||
import { AWSError } from "aws-sdk";
|
||||
import { CreateSecretResponse, SecretListEntry, SecretValueEntry } from "aws-sdk/clients/secretsmanager";
|
||||
import {
|
||||
CreateSecretResponse,
|
||||
DescribeSecretResponse,
|
||||
SecretListEntry,
|
||||
SecretValueEntry,
|
||||
Tag
|
||||
} from "aws-sdk/clients/secretsmanager";
|
||||
|
||||
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums";
|
||||
@@ -21,6 +33,7 @@ import { TAwsSecretsManagerSyncWithCredentials } from "./aws-secrets-manager-syn
|
||||
|
||||
type TAwsSecretsRecord = Record<string, SecretListEntry>;
|
||||
type TAwsSecretValuesRecord = Record<string, SecretValueEntry>;
|
||||
type TAwsSecretDescriptionsRecord = Record<string, DescribeSecretResponse>;
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
const BATCH_SIZE = 20;
|
||||
@@ -135,6 +148,46 @@ const getSecretValuesRecord = async (
|
||||
return awsSecretValuesRecord;
|
||||
};
|
||||
|
||||
const describeSecret = async (
|
||||
client: SecretsManagerClient,
|
||||
input: DescribeSecretCommandInput,
|
||||
attempt = 0
|
||||
): Promise<DescribeSecretResponse> => {
|
||||
try {
|
||||
return await client.send(new DescribeSecretCommand(input));
|
||||
} catch (error) {
|
||||
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
|
||||
// retry
|
||||
return describeSecret(client, input, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getSecretDescriptionsRecord = async (
|
||||
client: SecretsManagerClient,
|
||||
awsSecretsRecord: TAwsSecretsRecord
|
||||
): Promise<TAwsSecretDescriptionsRecord> => {
|
||||
const awsSecretDescriptionsRecord: TAwsSecretValuesRecord = {};
|
||||
|
||||
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
||||
try {
|
||||
awsSecretDescriptionsRecord[secretKey] = await describeSecret(client, {
|
||||
SecretId: secretKey
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
secretKey,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return awsSecretDescriptionsRecord;
|
||||
};
|
||||
|
||||
const createSecret = async (
|
||||
client: SecretsManagerClient,
|
||||
input: CreateSecretCommandInput,
|
||||
@@ -189,9 +242,71 @@ const deleteSecret = async (
|
||||
}
|
||||
};
|
||||
|
||||
const addTags = async (
|
||||
client: SecretsManagerClient,
|
||||
secretKey: string,
|
||||
tags: Tag[],
|
||||
attempt = 0
|
||||
): Promise<TagResourceCommandOutput> => {
|
||||
try {
|
||||
return await client.send(new TagResourceCommand({ SecretId: secretKey, Tags: tags }));
|
||||
} catch (error) {
|
||||
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
|
||||
// retry
|
||||
return addTags(client, secretKey, tags, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const removeTags = async (
|
||||
client: SecretsManagerClient,
|
||||
secretKey: string,
|
||||
tagKeys: string[],
|
||||
attempt = 0
|
||||
): Promise<UntagResourceCommandOutput> => {
|
||||
try {
|
||||
return await client.send(new UntagResourceCommand({ SecretId: secretKey, TagKeys: tagKeys }));
|
||||
} catch (error) {
|
||||
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
|
||||
// retry
|
||||
return removeTags(client, secretKey, tagKeys, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const processTags = ({
|
||||
syncTagsRecord,
|
||||
awsTagsRecord
|
||||
}: {
|
||||
syncTagsRecord: Record<string, string>;
|
||||
awsTagsRecord: Record<string, string>;
|
||||
}) => {
|
||||
const tagsToAdd: Tag[] = [];
|
||||
const tagKeysToRemove: string[] = [];
|
||||
|
||||
for (const syncEntry of Object.entries(syncTagsRecord)) {
|
||||
const [syncKey, syncValue] = syncEntry;
|
||||
|
||||
if (!(syncKey in awsTagsRecord) || syncValue !== awsTagsRecord[syncKey])
|
||||
tagsToAdd.push({ Key: syncKey, Value: syncValue });
|
||||
}
|
||||
|
||||
for (const awsKey of Object.keys(awsTagsRecord)) {
|
||||
if (!(awsKey in syncTagsRecord)) tagKeysToRemove.push(awsKey);
|
||||
}
|
||||
|
||||
return { tagsToAdd, tagKeysToRemove };
|
||||
};
|
||||
|
||||
export const AwsSecretsManagerSyncFns = {
|
||||
syncSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { destinationConfig } = secretSync;
|
||||
const { destinationConfig, syncOptions } = secretSync;
|
||||
|
||||
const client = await getSecretsManagerClient(secretSync);
|
||||
|
||||
@@ -199,9 +314,13 @@ export const AwsSecretsManagerSyncFns = {
|
||||
|
||||
const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord);
|
||||
|
||||
const awsDescriptionsRecord = await getSecretDescriptionsRecord(client, awsSecretsRecord);
|
||||
|
||||
const syncTagsRecord = Object.fromEntries(syncOptions.tags?.map((tag) => [tag.key, tag.value]) ?? []);
|
||||
|
||||
if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) {
|
||||
for await (const entry of Object.entries(secretMap)) {
|
||||
const [key, { value }] = entry;
|
||||
const [key, { value, secretMetadata }] = entry;
|
||||
|
||||
// skip secrets that don't have a value set
|
||||
if (!value) {
|
||||
@@ -211,15 +330,29 @@ export const AwsSecretsManagerSyncFns = {
|
||||
|
||||
if (awsSecretsRecord[key]) {
|
||||
// skip secrets that haven't changed
|
||||
if (awsValuesRecord[key]?.SecretString === value) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
if (
|
||||
awsValuesRecord[key]?.SecretString !== value ||
|
||||
(syncOptions.keyId ?? "alias/aws/secretsmanager") !== awsDescriptionsRecord[key]?.KmsKeyId
|
||||
) {
|
||||
try {
|
||||
await updateSecret(client, {
|
||||
SecretId: key,
|
||||
SecretString: value,
|
||||
KmsKeyId: syncOptions.keyId
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
try {
|
||||
await updateSecret(client, {
|
||||
SecretId: key,
|
||||
SecretString: value
|
||||
await createSecret(client, {
|
||||
Name: key,
|
||||
SecretString: value,
|
||||
KmsKeyId: syncOptions.keyId
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
@@ -227,12 +360,34 @@ export const AwsSecretsManagerSyncFns = {
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
const { tagsToAdd, tagKeysToRemove } = processTags({
|
||||
syncTagsRecord: {
|
||||
// configured sync tags take preference over secret metadata
|
||||
...(syncOptions.syncSecretMetadataAsTags &&
|
||||
Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])),
|
||||
...syncTagsRecord
|
||||
},
|
||||
awsTagsRecord: Object.fromEntries(
|
||||
awsDescriptionsRecord[key]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? []
|
||||
)
|
||||
});
|
||||
|
||||
if (tagsToAdd.length) {
|
||||
try {
|
||||
await createSecret(client, {
|
||||
Name: key,
|
||||
SecretString: value
|
||||
await addTags(client, key, tagsToAdd);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tagKeysToRemove.length) {
|
||||
try {
|
||||
await removeTags(client, key, tagKeysToRemove);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
@@ -264,15 +419,46 @@ export const AwsSecretsManagerSyncFns = {
|
||||
if (awsValuesRecord[destinationConfig.secretName]) {
|
||||
await updateSecret(client, {
|
||||
SecretId: destinationConfig.secretName,
|
||||
SecretString: secretValue
|
||||
SecretString: secretValue,
|
||||
KmsKeyId: syncOptions.keyId
|
||||
});
|
||||
} else {
|
||||
await createSecret(client, {
|
||||
Name: destinationConfig.secretName,
|
||||
SecretString: secretValue
|
||||
SecretString: secretValue,
|
||||
KmsKeyId: syncOptions.keyId
|
||||
});
|
||||
}
|
||||
|
||||
const { tagsToAdd, tagKeysToRemove } = processTags({
|
||||
syncTagsRecord,
|
||||
awsTagsRecord: Object.fromEntries(
|
||||
awsDescriptionsRecord[destinationConfig.secretName]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? []
|
||||
)
|
||||
});
|
||||
|
||||
if (tagsToAdd.length) {
|
||||
try {
|
||||
await addTags(client, destinationConfig.secretName, tagsToAdd);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: destinationConfig.secretName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tagKeysToRemove.length) {
|
||||
try {
|
||||
await removeTags(client, destinationConfig.secretName, tagKeysToRemove);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: destinationConfig.secretName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
||||
if (secretKey === destinationConfig.secretName) {
|
||||
// eslint-disable-next-line no-continue
|
||||
|
||||
@@ -39,11 +39,51 @@ const AwsSecretsManagerSyncDestinationConfigSchema = z
|
||||
})
|
||||
);
|
||||
|
||||
const AwsSecretsManagerSyncOptionsSchema = z.object({
|
||||
keyId: z
|
||||
.string()
|
||||
.regex(/^([a-zA-Z0-9:/_-]+)$/, "Invalid KMS Key ID")
|
||||
.min(1, "Invalid KMS Key ID")
|
||||
.max(256, "Invalid KMS Key ID")
|
||||
.optional()
|
||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.keyId),
|
||||
tags: z
|
||||
.object({
|
||||
key: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Invalid tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.min(1, "Tag key required")
|
||||
.max(128, "Tag key cannot exceed 128 characters"),
|
||||
value: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Invalid tag value: tag 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)
|
||||
.refine((items) => new Set(items.map((item) => item.key)).size === items.length, {
|
||||
message: "Tag keys must be unique"
|
||||
})
|
||||
.optional()
|
||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.tags),
|
||||
syncSecretMetadataAsTags: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.syncSecretMetadataAsTags)
|
||||
});
|
||||
|
||||
const AwsSecretsManagerSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(
|
||||
SecretSync.AWSSecretsManager,
|
||||
AwsSecretsManagerSyncOptionsConfig
|
||||
AwsSecretsManagerSyncOptionsConfig,
|
||||
AwsSecretsManagerSyncOptionsSchema
|
||||
).extend({
|
||||
destination: z.literal(SecretSync.AWSSecretsManager),
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||
@@ -51,17 +91,43 @@ export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(
|
||||
|
||||
export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.AWSSecretsManager,
|
||||
AwsSecretsManagerSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||
});
|
||||
AwsSecretsManagerSyncOptionsConfig,
|
||||
AwsSecretsManagerSyncOptionsSchema
|
||||
)
|
||||
.extend({
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||
})
|
||||
.superRefine((sync, ctx) => {
|
||||
if (
|
||||
sync.destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne &&
|
||||
sync.syncOptions.syncSecretMetadataAsTags
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Syncing secret metadata is not supported with "Many-to-One" mapping behavior.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.AWSSecretsManager,
|
||||
AwsSecretsManagerSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
AwsSecretsManagerSyncOptionsConfig,
|
||||
AwsSecretsManagerSyncOptionsSchema
|
||||
)
|
||||
.extend({
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional()
|
||||
})
|
||||
.superRefine((sync, ctx) => {
|
||||
if (
|
||||
sync.destinationConfig?.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne &&
|
||||
sync.syncOptions.syncSecretMetadataAsTags
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Syncing secret metadata is not supported with "Many-to-One" mapping behavior.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const AwsSecretsManagerSyncListItemSchema = z.object({
|
||||
name: z.literal("AWS Secrets Manager"),
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 832 KiB After Width: | Height: | Size: 878 KiB |
@@ -88,10 +88,10 @@ Infisical supports two methods for connecting to AWS.
|
||||
"secretsmanager:DescribeSecret",
|
||||
"secretsmanager:TagResource",
|
||||
"secretsmanager:UntagResource",
|
||||
"kms:ListKeys",
|
||||
"kms:ListAliases",
|
||||
"kms:Encrypt",
|
||||
"kms:Decrypt"
|
||||
"kms:ListKeys", // if you need to specify the KMS key
|
||||
"kms:ListAliases", // if you need to specify the KMS key
|
||||
"kms:Encrypt", // if you need to specify the KMS key
|
||||
"kms:Decrypt" // if you need to specify the KMS key
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
@@ -231,10 +231,10 @@ Infisical supports two methods for connecting to AWS.
|
||||
"secretsmanager:DescribeSecret",
|
||||
"secretsmanager:TagResource",
|
||||
"secretsmanager:UntagResource",
|
||||
"kms:ListKeys",
|
||||
"kms:ListAliases",
|
||||
"kms:Encrypt",
|
||||
"kms:Decrypt"
|
||||
"kms:ListKeys", // if you need to specify the KMS key
|
||||
"kms:ListAliases", // if you need to specify the KMS key
|
||||
"kms:Encrypt", // if you need to specify the KMS key
|
||||
"kms:Decrypt" // if you need to specify the KMS key
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
|
||||
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
|
||||
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Parameter Store when keys conflict.
|
||||
- **Import Secrets (Prioritize AWS Parameter Store)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Parameter Store over Infisical when keys conflict.
|
||||
- **Key ID**: The AWS KMS key ID or alias to encrypt parameters with.
|
||||
- **KMS Key**: The AWS KMS key ID or alias to encrypt parameters with.
|
||||
- **Tags**: Optional resource tags to add to parameters synced by Infisical.
|
||||
- **Sync Secret Metadata as Resource Tags**: If enabled, metadata attached to secrets will be added as resource tags to parameters synced by Infisical.
|
||||
<Note>Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict.</Note>
|
||||
|
||||
@@ -43,6 +43,9 @@ description: "Learn how to configure an AWS Secrets Manager Sync for Infisical."
|
||||
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
|
||||
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict.
|
||||
- **Import Secrets (Prioritize AWS Secrets Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict.
|
||||
- **KMS Key**: The AWS KMS key ID or alias to encrypt secrets with.
|
||||
- **Tags**: Optional tags to add to secrets synced by Infisical.
|
||||
- **Sync Secret Metadata as Tags**: If enabled, metadata attached to secrets will be added as tags to secrets synced by Infisical.
|
||||
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||
|
||||
6. Configure the **Details** of your Secrets Manager Sync, then click **Next**.
|
||||
|
||||
@@ -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">
|
||||
"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>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user