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.",
|
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.",
|
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.`
|
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: {
|
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: _.:/=+@-"
|
"Invalid resource tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||||
)
|
)
|
||||||
.min(1, "Resource tag key required")
|
.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
|
value: z
|
||||||
.string()
|
.string()
|
||||||
.regex(
|
.regex(
|
||||||
@@ -50,7 +50,7 @@ const AwsParameterStoreSyncOptionsSchema = z.object({
|
|||||||
.array()
|
.array()
|
||||||
.max(50)
|
.max(50)
|
||||||
.refine((items) => new Set(items.map((item) => item.key)).size === items.length, {
|
.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()
|
.optional()
|
||||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.tags),
|
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.tags),
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
|
import { UntagResourceCommandOutput } from "@aws-sdk/client-kms";
|
||||||
import {
|
import {
|
||||||
BatchGetSecretValueCommand,
|
BatchGetSecretValueCommand,
|
||||||
CreateSecretCommand,
|
CreateSecretCommand,
|
||||||
CreateSecretCommandInput,
|
CreateSecretCommandInput,
|
||||||
DeleteSecretCommand,
|
DeleteSecretCommand,
|
||||||
DeleteSecretResponse,
|
DeleteSecretResponse,
|
||||||
|
DescribeSecretCommand,
|
||||||
|
DescribeSecretCommandInput,
|
||||||
ListSecretsCommand,
|
ListSecretsCommand,
|
||||||
SecretsManagerClient,
|
SecretsManagerClient,
|
||||||
|
TagResourceCommand,
|
||||||
|
TagResourceCommandOutput,
|
||||||
|
UntagResourceCommand,
|
||||||
UpdateSecretCommand,
|
UpdateSecretCommand,
|
||||||
UpdateSecretCommandInput
|
UpdateSecretCommandInput
|
||||||
} from "@aws-sdk/client-secrets-manager";
|
} from "@aws-sdk/client-secrets-manager";
|
||||||
import { AWSError } from "aws-sdk";
|
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 { 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";
|
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 TAwsSecretsRecord = Record<string, SecretListEntry>;
|
||||||
type TAwsSecretValuesRecord = Record<string, SecretValueEntry>;
|
type TAwsSecretValuesRecord = Record<string, SecretValueEntry>;
|
||||||
|
type TAwsSecretDescriptionsRecord = Record<string, DescribeSecretResponse>;
|
||||||
|
|
||||||
const MAX_RETRIES = 5;
|
const MAX_RETRIES = 5;
|
||||||
const BATCH_SIZE = 20;
|
const BATCH_SIZE = 20;
|
||||||
@@ -135,6 +148,46 @@ const getSecretValuesRecord = async (
|
|||||||
return awsSecretValuesRecord;
|
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 (
|
const createSecret = async (
|
||||||
client: SecretsManagerClient,
|
client: SecretsManagerClient,
|
||||||
input: CreateSecretCommandInput,
|
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 = {
|
export const AwsSecretsManagerSyncFns = {
|
||||||
syncSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => {
|
syncSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => {
|
||||||
const { destinationConfig } = secretSync;
|
const { destinationConfig, syncOptions } = secretSync;
|
||||||
|
|
||||||
const client = await getSecretsManagerClient(secretSync);
|
const client = await getSecretsManagerClient(secretSync);
|
||||||
|
|
||||||
@@ -199,9 +314,13 @@ export const AwsSecretsManagerSyncFns = {
|
|||||||
|
|
||||||
const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord);
|
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) {
|
if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) {
|
||||||
for await (const entry of Object.entries(secretMap)) {
|
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
|
// skip secrets that don't have a value set
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -211,15 +330,29 @@ export const AwsSecretsManagerSyncFns = {
|
|||||||
|
|
||||||
if (awsSecretsRecord[key]) {
|
if (awsSecretsRecord[key]) {
|
||||||
// skip secrets that haven't changed
|
// skip secrets that haven't changed
|
||||||
if (awsValuesRecord[key]?.SecretString === value) {
|
if (
|
||||||
// eslint-disable-next-line no-continue
|
awsValuesRecord[key]?.SecretString !== value ||
|
||||||
continue;
|
(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 {
|
try {
|
||||||
await updateSecret(client, {
|
await createSecret(client, {
|
||||||
SecretId: key,
|
Name: key,
|
||||||
SecretString: value
|
SecretString: value,
|
||||||
|
KmsKeyId: syncOptions.keyId
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new SecretSyncError({
|
throw new SecretSyncError({
|
||||||
@@ -227,12 +360,34 @@ export const AwsSecretsManagerSyncFns = {
|
|||||||
secretKey: key
|
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 {
|
try {
|
||||||
await createSecret(client, {
|
await addTags(client, key, tagsToAdd);
|
||||||
Name: key,
|
} catch (error) {
|
||||||
SecretString: value
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: key
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagKeysToRemove.length) {
|
||||||
|
try {
|
||||||
|
await removeTags(client, key, tagKeysToRemove);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new SecretSyncError({
|
throw new SecretSyncError({
|
||||||
error,
|
error,
|
||||||
@@ -264,15 +419,46 @@ export const AwsSecretsManagerSyncFns = {
|
|||||||
if (awsValuesRecord[destinationConfig.secretName]) {
|
if (awsValuesRecord[destinationConfig.secretName]) {
|
||||||
await updateSecret(client, {
|
await updateSecret(client, {
|
||||||
SecretId: destinationConfig.secretName,
|
SecretId: destinationConfig.secretName,
|
||||||
SecretString: secretValue
|
SecretString: secretValue,
|
||||||
|
KmsKeyId: syncOptions.keyId
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await createSecret(client, {
|
await createSecret(client, {
|
||||||
Name: destinationConfig.secretName,
|
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)) {
|
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
||||||
if (secretKey === destinationConfig.secretName) {
|
if (secretKey === destinationConfig.secretName) {
|
||||||
// eslint-disable-next-line no-continue
|
// 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 };
|
const AwsSecretsManagerSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||||
|
|
||||||
export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(
|
export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(
|
||||||
SecretSync.AWSSecretsManager,
|
SecretSync.AWSSecretsManager,
|
||||||
AwsSecretsManagerSyncOptionsConfig
|
AwsSecretsManagerSyncOptionsConfig,
|
||||||
|
AwsSecretsManagerSyncOptionsSchema
|
||||||
).extend({
|
).extend({
|
||||||
destination: z.literal(SecretSync.AWSSecretsManager),
|
destination: z.literal(SecretSync.AWSSecretsManager),
|
||||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||||
@@ -51,17 +91,43 @@ export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(
|
|||||||
|
|
||||||
export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||||
SecretSync.AWSSecretsManager,
|
SecretSync.AWSSecretsManager,
|
||||||
AwsSecretsManagerSyncOptionsConfig
|
AwsSecretsManagerSyncOptionsConfig,
|
||||||
).extend({
|
AwsSecretsManagerSyncOptionsSchema
|
||||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
)
|
||||||
});
|
.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(
|
export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||||
SecretSync.AWSSecretsManager,
|
SecretSync.AWSSecretsManager,
|
||||||
AwsSecretsManagerSyncOptionsConfig
|
AwsSecretsManagerSyncOptionsConfig,
|
||||||
).extend({
|
AwsSecretsManagerSyncOptionsSchema
|
||||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional()
|
)
|
||||||
});
|
.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({
|
export const AwsSecretsManagerSyncListItemSchema = z.object({
|
||||||
name: z.literal("AWS Secrets Manager"),
|
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:DescribeSecret",
|
||||||
"secretsmanager:TagResource",
|
"secretsmanager:TagResource",
|
||||||
"secretsmanager:UntagResource",
|
"secretsmanager:UntagResource",
|
||||||
"kms:ListKeys",
|
"kms:ListKeys", // if you need to specify the KMS key
|
||||||
"kms:ListAliases",
|
"kms:ListAliases", // if you need to specify the KMS key
|
||||||
"kms:Encrypt",
|
"kms:Encrypt", // if you need to specify the KMS key
|
||||||
"kms:Decrypt"
|
"kms:Decrypt" // if you need to specify the KMS key
|
||||||
],
|
],
|
||||||
"Resource": "*"
|
"Resource": "*"
|
||||||
}
|
}
|
||||||
@@ -231,10 +231,10 @@ Infisical supports two methods for connecting to AWS.
|
|||||||
"secretsmanager:DescribeSecret",
|
"secretsmanager:DescribeSecret",
|
||||||
"secretsmanager:TagResource",
|
"secretsmanager:TagResource",
|
||||||
"secretsmanager:UntagResource",
|
"secretsmanager:UntagResource",
|
||||||
"kms:ListKeys",
|
"kms:ListKeys", // if you need to specify the KMS key
|
||||||
"kms:ListAliases",
|
"kms:ListAliases", // if you need to specify the KMS key
|
||||||
"kms:Encrypt",
|
"kms:Encrypt", // if you need to specify the KMS key
|
||||||
"kms:Decrypt"
|
"kms:Decrypt" // if you need to specify the KMS key
|
||||||
],
|
],
|
||||||
"Resource": "*"
|
"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.
|
- **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 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.
|
- **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.
|
- **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.
|
- **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>
|
<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.
|
- **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 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.
|
- **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.
|
- **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**.
|
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}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Switch
|
<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"
|
id="auto-sync-enabled"
|
||||||
thumbClassName="bg-mineshaft-800"
|
thumbClassName="bg-mineshaft-800"
|
||||||
onCheckedChange={onChange}
|
onCheckedChange={onChange}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { TSecretSyncForm } from "../schemas";
|
|||||||
import { AwsRegionSelect } from "./shared";
|
import { AwsRegionSelect } from "./shared";
|
||||||
|
|
||||||
export const AwsSecretsManagerSyncFields = () => {
|
export const AwsSecretsManagerSyncFields = () => {
|
||||||
const { control, watch } = useFormContext<
|
const { control, watch, setValue } = useFormContext<
|
||||||
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
|
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
|
||||||
>();
|
>();
|
||||||
|
|
||||||
@@ -59,7 +59,10 @@ export const AwsSecretsManagerSyncFields = () => {
|
|||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
value={value}
|
value={value}
|
||||||
onValueChange={(val) => onChange(val)}
|
onValueChange={(val) => {
|
||||||
|
onChange(val);
|
||||||
|
setValue("syncOptions.syncSecretMetadataAsTags", false);
|
||||||
|
}}
|
||||||
className="w-full border border-mineshaft-500 capitalize"
|
className="w-full border border-mineshaft-500 capitalize"
|
||||||
position="popper"
|
position="popper"
|
||||||
placeholder="Select an option..."
|
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 { TSecretSyncForm } from "../schemas";
|
||||||
import { AwsParameterStoreSyncOptionsFields } from "./AwsParameterStoreSyncOptionsFields";
|
import { AwsParameterStoreSyncOptionsFields } from "./AwsParameterStoreSyncOptionsFields";
|
||||||
|
import { AwsSecretsManagerSyncOptionsFields } from "./AwsSecretsManagerSyncOptionsFields";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
hideInitialSync?: boolean;
|
hideInitialSync?: boolean;
|
||||||
@@ -30,6 +31,8 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
|||||||
AdditionalSyncOptionsFieldsComponent = <AwsParameterStoreSyncOptionsFields />;
|
AdditionalSyncOptionsFieldsComponent = <AwsParameterStoreSyncOptionsFields />;
|
||||||
break;
|
break;
|
||||||
case SecretSync.AWSSecretsManager:
|
case SecretSync.AWSSecretsManager:
|
||||||
|
AdditionalSyncOptionsFieldsComponent = <AwsSecretsManagerSyncOptionsFields />;
|
||||||
|
break;
|
||||||
case SecretSync.GitHub:
|
case SecretSync.GitHub:
|
||||||
case SecretSync.GCPSecretManager:
|
case SecretSync.GCPSecretManager:
|
||||||
case SecretSync.AzureKeyVault:
|
case SecretSync.AzureKeyVault:
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useFormContext } from "react-hook-form";
|
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 { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
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 { AWS_REGIONS } from "@app/helpers/appConnections";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
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,
|
AwsParameterStoreDestinationReviewFields,
|
||||||
AwsParameterStoreSyncOptionsReviewFields
|
AwsParameterStoreSyncOptionsReviewFields
|
||||||
} from "./AwsParameterStoreSyncReviewFields";
|
} from "./AwsParameterStoreSyncReviewFields";
|
||||||
import { AwsSecretsManagerSyncReviewFields } from "./AwsSecretsManagerSyncReviewFields";
|
import {
|
||||||
|
AwsSecretsManagerSyncOptionsReviewFields,
|
||||||
|
AwsSecretsManagerSyncReviewFields
|
||||||
|
} from "./AwsSecretsManagerSyncReviewFields";
|
||||||
import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields";
|
import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields";
|
||||||
import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
|
import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
|
||||||
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
|
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
|
||||||
@@ -47,6 +50,7 @@ export const SecretSyncReviewFields = () => {
|
|||||||
break;
|
break;
|
||||||
case SecretSync.AWSSecretsManager:
|
case SecretSync.AWSSecretsManager:
|
||||||
DestinationFieldsComponent = <AwsSecretsManagerSyncReviewFields />;
|
DestinationFieldsComponent = <AwsSecretsManagerSyncReviewFields />;
|
||||||
|
AdditionalSyncOptionsFieldsComponent = <AwsSecretsManagerSyncOptionsReviewFields />;
|
||||||
break;
|
break;
|
||||||
case SecretSync.GitHub:
|
case SecretSync.GitHub:
|
||||||
DestinationFieldsComponent = <GitHubSyncReviewFields />;
|
DestinationFieldsComponent = <GitHubSyncReviewFields />;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const AwsParameterStoreSyncDestinationSchema = BaseSecretSyncSchema(
|
|||||||
"Keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
"Keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||||
)
|
)
|
||||||
.min(1, "Key required")
|
.min(1, "Key required")
|
||||||
.max(128, "AWS tag name cannot exceed 128 characters"),
|
.max(128, "Tag key cannot exceed 128 characters"),
|
||||||
value: z
|
value: z
|
||||||
.string()
|
.string()
|
||||||
.regex(
|
.regex(
|
||||||
|
|||||||
@@ -4,7 +4,33 @@ import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas
|
|||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
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({
|
z.object({
|
||||||
destination: z.literal(SecretSync.AWSSecretsManager),
|
destination: z.literal(SecretSync.AWSSecretsManager),
|
||||||
destinationConfig: z
|
destinationConfig: z
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
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 & {
|
export type TAwsSecretsManagerSync = TRootSecretSync & {
|
||||||
destination: SecretSync.AWSSecretsManager;
|
destination: SecretSync.AWSSecretsManager;
|
||||||
@@ -19,6 +19,11 @@ export type TAwsSecretsManagerSync = TRootSecretSync & {
|
|||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
syncOptions: RootSyncOptions & {
|
||||||
|
keyId?: string;
|
||||||
|
tags?: { key: string; value?: string }[];
|
||||||
|
syncSecretMetadataAsTags?: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
export enum AwsSecretsManagerSyncMappingBehavior {
|
export enum AwsSecretsManagerSyncMappingBehavior {
|
||||||
OneToOne = "one-to-one",
|
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 { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
import { AwsParameterStoreSyncOptionsSection } from "./AwsParameterStoreSyncOptionsSection";
|
import { AwsParameterStoreSyncOptionsSection } from "./AwsParameterStoreSyncOptionsSection";
|
||||||
|
import { AwsSecretsManagerSyncOptionsSection } from "./AwsSecretsManagerSyncOptionsSection";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
secretSync: TSecretSync;
|
secretSync: TSecretSync;
|
||||||
@@ -36,6 +37,10 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case SecretSync.AWSSecretsManager:
|
case SecretSync.AWSSecretsManager:
|
||||||
|
AdditionalSyncOptionsComponent = (
|
||||||
|
<AwsSecretsManagerSyncOptionsSection secretSync={secretSync} />
|
||||||
|
);
|
||||||
|
break;
|
||||||
case SecretSync.GitHub:
|
case SecretSync.GitHub:
|
||||||
case SecretSync.GCPSecretManager:
|
case SecretSync.GCPSecretManager:
|
||||||
case SecretSync.AzureKeyVault:
|
case SecretSync.AzureKeyVault:
|
||||||
|
|||||||
Reference in New Issue
Block a user