Merge pull request #3134 from Infisical/aws-secrets-manager-additional-features

Feature: AWS Syncs - Additional Features
This commit is contained in:
Scott Wilson
2025-02-21 08:23:06 -08:00
committed by GitHub
54 changed files with 2000 additions and 369 deletions
+12
View File
@@ -1722,6 +1722,18 @@ export const SecretSyncs = {
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.` initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`
}; };
}, },
ADDITIONAL_SYNC_OPTIONS: {
AWS_PARAMETER_STORE: {
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: { DESTINATION_CONFIG: {
AWS_PARAMETER_STORE: { AWS_PARAMETER_STORE: {
region: "The AWS region to sync secrets to.", region: "The AWS region to sync secrets to.",
@@ -1,13 +1,19 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { z } from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
import { import {
CreateAwsConnectionSchema, CreateAwsConnectionSchema,
SanitizedAwsConnectionSchema, SanitizedAwsConnectionSchema,
UpdateAwsConnectionSchema UpdateAwsConnectionSchema
} from "@app/services/app-connection/aws"; } from "@app/services/app-connection/aws";
import { AuthMode } from "@app/services/auth/auth-type";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerAwsConnectionRouter = async (server: FastifyZodProvider) => export const registerAwsConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({ registerAppConnectionEndpoints({
app: AppConnection.AWS, app: AppConnection.AWS,
server, server,
@@ -15,3 +21,42 @@ export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
createSchema: CreateAwsConnectionSchema, createSchema: CreateAwsConnectionSchema,
updateSchema: UpdateAwsConnectionSchema updateSchema: UpdateAwsConnectionSchema
}); });
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/kms-keys`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
querystring: z.object({
region: z.nativeEnum(AWSRegion),
destination: z.enum([SecretSync.AWSParameterStore, SecretSync.AWSSecretsManager])
}),
response: {
200: z.object({
kmsKeys: z.object({ alias: z.string(), id: z.string() }).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const kmsKeys = await server.services.appConnection.aws.listKmsKeys(
{
connectionId,
...req.query
},
req.permission
);
return { kmsKeys };
}
});
};
@@ -22,18 +22,19 @@ import {
TUpdateAppConnectionDTO, TUpdateAppConnectionDTO,
TValidateAppConnectionCredentials TValidateAppConnectionCredentials
} from "@app/services/app-connection/app-connection-types"; } from "@app/services/app-connection/app-connection-types";
import { ValidateAwsConnectionCredentialsSchema } from "@app/services/app-connection/aws";
import { ValidateDatabricksConnectionCredentialsSchema } from "@app/services/app-connection/databricks";
import { databricksConnectionService } from "@app/services/app-connection/databricks/databricks-connection-service";
import { ValidateGitHubConnectionCredentialsSchema } from "@app/services/app-connection/github";
import { githubConnectionService } from "@app/services/app-connection/github/github-connection-service";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TAppConnectionDALFactory } from "./app-connection-dal"; import { TAppConnectionDALFactory } from "./app-connection-dal";
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
import { awsConnectionService } from "./aws/aws-connection-service";
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration"; import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault"; import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
import { databricksConnectionService } from "./databricks/databricks-connection-service";
import { ValidateGcpConnectionCredentialsSchema } from "./gcp"; import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
import { gcpConnectionService } from "./gcp/gcp-connection-service"; import { gcpConnectionService } from "./gcp/gcp-connection-service";
import { ValidateGitHubConnectionCredentialsSchema } from "./github";
import { githubConnectionService } from "./github/github-connection-service";
export type TAppConnectionServiceFactoryDep = { export type TAppConnectionServiceFactoryDep = {
appConnectionDAL: TAppConnectionDALFactory; appConnectionDAL: TAppConnectionDALFactory;
@@ -369,6 +370,7 @@ export const appConnectionServiceFactory = ({
listAvailableAppConnectionsForUser, listAvailableAppConnectionsForUser,
github: githubConnectionService(connectAppConnectionById), github: githubConnectionService(connectAppConnectionById),
gcp: gcpConnectionService(connectAppConnectionById), gcp: gcpConnectionService(connectAppConnectionById),
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService) databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
aws: awsConnectionService(connectAppConnectionById)
}; };
}; };
@@ -1,3 +1,4 @@
import { AWSRegion } from "@app/services/app-connection/app-connection-enums";
import { import {
TAwsConnection, TAwsConnection,
TAwsConnectionConfig, TAwsConnectionConfig,
@@ -16,6 +17,7 @@ import {
TGitHubConnectionInput, TGitHubConnectionInput,
TValidateGitHubConnectionCredentials TValidateGitHubConnectionCredentials
} from "@app/services/app-connection/github"; } from "@app/services/app-connection/github";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { import {
TAzureAppConfigurationConnection, TAzureAppConfigurationConnection,
@@ -73,3 +75,9 @@ export type TValidateAppConnectionCredentials =
| TValidateAzureKeyVaultConnectionCredentials | TValidateAzureKeyVaultConnectionCredentials
| TValidateAzureAppConfigurationConnectionCredentials | TValidateAzureAppConfigurationConnectionCredentials
| TValidateDatabricksConnectionCredentials; | TValidateDatabricksConnectionCredentials;
export type TListAwsConnectionKmsKeys = {
connectionId: string;
region: AWSRegion;
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
};
@@ -0,0 +1,88 @@
import AWS from "aws-sdk";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { TListAwsConnectionKmsKeys } from "@app/services/app-connection/app-connection-types";
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TAwsConnection>;
const listAwsKmsKeys = async (
appConnection: TAwsConnection,
{ region, destination }: Pick<TListAwsConnectionKmsKeys, "region" | "destination">
) => {
const { credentials } = await getAwsConnectionConfig(appConnection, region);
const awsKms = new AWS.KMS({
credentials,
region
});
const aliasEntries: AWS.KMS.AliasList = [];
let aliasMarker: string | undefined;
do {
// eslint-disable-next-line no-await-in-loop
const response = await awsKms.listAliases({ Limit: 100, Marker: aliasMarker }).promise();
aliasEntries.push(...(response.Aliases || []));
aliasMarker = response.NextMarker;
} while (aliasMarker);
const keyMetadataRecord: Record<string, AWS.KMS.KeyMetadata | undefined> = {};
for await (const aliasEntry of aliasEntries) {
if (aliasEntry.TargetKeyId) {
const keyDescription = await awsKms.describeKey({ KeyId: aliasEntry.TargetKeyId }).promise();
keyMetadataRecord[aliasEntry.TargetKeyId] = keyDescription.KeyMetadata;
}
}
const validAliasEntries = aliasEntries.filter((aliasEntry) => {
if (!aliasEntry.TargetKeyId) return false;
if (destination === SecretSync.AWSParameterStore && aliasEntry.AliasName === "alias/aws/ssm") return true;
if (destination === SecretSync.AWSSecretsManager && aliasEntry.AliasName === "alias/aws/secretsmanager")
return true;
if (aliasEntry.AliasName?.includes("alias/aws/")) return false;
const keyMetadata = keyMetadataRecord[aliasEntry.TargetKeyId];
if (!keyMetadata || keyMetadata.KeyUsage !== "ENCRYPT_DECRYPT" || keyMetadata.KeySpec !== "SYMMETRIC_DEFAULT")
return false;
return true;
});
const kmsKeys = validAliasEntries.map((aliasEntry) => {
return {
id: aliasEntry.TargetKeyId!,
alias: aliasEntry.AliasName!
};
});
return kmsKeys;
};
export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listKmsKeys = async (
{ connectionId, region, destination }: TListAwsConnectionKmsKeys,
actor: OrgServiceActor
) => {
const appConnection = await getAppConnection(AppConnection.AWS, connectionId, actor);
const kmsKeys = await listAwsKmsKeys(appConnection, { region, destination });
return kmsKeys;
};
return {
listKmsKeys
};
};
@@ -7,6 +7,8 @@ import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { TAwsParameterStoreSyncWithCredentials } from "./aws-parameter-store-sync-types"; import { TAwsParameterStoreSyncWithCredentials } from "./aws-parameter-store-sync-types";
type TAWSParameterStoreRecord = Record<string, AWS.SSM.Parameter>; type TAWSParameterStoreRecord = Record<string, AWS.SSM.Parameter>;
type TAWSParameterStoreMetadataRecord = Record<string, AWS.SSM.ParameterMetadata>;
type TAWSParameterStoreTagsRecord = Record<string, Record<string, string>>;
const MAX_RETRIES = 5; const MAX_RETRIES = 5;
const BATCH_SIZE = 10; const BATCH_SIZE = 10;
@@ -80,6 +82,129 @@ const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSPara
return awsParameterStoreSecretsRecord; return awsParameterStoreSecretsRecord;
}; };
const getParameterMetadataByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSParameterStoreMetadataRecord> => {
const awsParameterStoreMetadataRecord: TAWSParameterStoreMetadataRecord = {};
let hasNext = true;
let nextToken: string | undefined;
let attempt = 0;
while (hasNext) {
try {
// eslint-disable-next-line no-await-in-loop
const parameters = await ssm
.describeParameters({
MaxResults: 10,
NextToken: nextToken,
ParameterFilters: [
{
Key: "Path",
Option: "OneLevel",
Values: [path]
}
]
})
.promise();
attempt = 0;
if (parameters.Parameters) {
parameters.Parameters.forEach((parameter) => {
if (parameter.Name) {
// no leading slash if path is '/'
const secKey = path.length > 1 ? parameter.Name.substring(path.length) : parameter.Name;
awsParameterStoreMetadataRecord[secKey] = parameter;
}
});
}
hasNext = Boolean(parameters.NextToken);
nextToken = parameters.NextToken;
} catch (e) {
if ((e as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
attempt += 1;
// eslint-disable-next-line no-await-in-loop
await sleep();
// eslint-disable-next-line no-continue
continue;
}
throw e;
}
}
return awsParameterStoreMetadataRecord;
};
const getParameterStoreTagsRecord = async (
ssm: AWS.SSM,
awsParameterStoreSecretsRecord: TAWSParameterStoreRecord,
needsTagsPermissions: boolean
): Promise<{ shouldManageTags: boolean; awsParameterStoreTagsRecord: TAWSParameterStoreTagsRecord }> => {
const awsParameterStoreTagsRecord: TAWSParameterStoreTagsRecord = {};
for await (const entry of Object.entries(awsParameterStoreSecretsRecord)) {
const [key, parameter] = entry;
if (!parameter.Name) {
// eslint-disable-next-line no-continue
continue;
}
try {
const tags = await ssm
.listTagsForResource({
ResourceType: "Parameter",
ResourceId: parameter.Name
})
.promise();
awsParameterStoreTagsRecord[key] = Object.fromEntries(tags.TagList?.map((tag) => [tag.Key, tag.Value]) ?? []);
} catch (e) {
// users aren't required to provide tag permissions to use sync so we handle gracefully if unauthorized
// and they aren't trying to configure tags
if ((e as AWSError).code === "AccessDeniedException") {
if (!needsTagsPermissions) {
return { shouldManageTags: false, awsParameterStoreTagsRecord: {} };
}
throw new SecretSyncError({
message:
"IAM role has inadequate permissions to manage resource tags. Ensure the following polices are present: ssm:ListTagsForResource, ssm:AddTagsToResource, and ssm:RemoveTagsFromResource",
shouldRetry: false
});
}
throw e;
}
}
return { shouldManageTags: true, awsParameterStoreTagsRecord };
};
const processParameterTags = ({
syncTagsRecord,
awsTagsRecord
}: {
syncTagsRecord: Record<string, string>;
awsTagsRecord: Record<string, string>;
}) => {
const tagsToAdd: AWS.SSM.TagList = [];
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 };
};
const putParameter = async ( const putParameter = async (
ssm: AWS.SSM, ssm: AWS.SSM,
params: AWS.SSM.PutParameterRequest, params: AWS.SSM.PutParameterRequest,
@@ -98,6 +223,42 @@ const putParameter = async (
} }
}; };
const addTagsToParameter = async (
ssm: AWS.SSM,
params: Omit<AWS.SSM.AddTagsToResourceRequest, "ResourceType">,
attempt = 0
): Promise<AWS.SSM.AddTagsToResourceResult> => {
try {
return await ssm.addTagsToResource({ ...params, ResourceType: "Parameter" }).promise();
} catch (error) {
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
await sleep();
// retry
return addTagsToParameter(ssm, params, attempt + 1);
}
throw error;
}
};
const removeTagsFromParameter = async (
ssm: AWS.SSM,
params: Omit<AWS.SSM.RemoveTagsFromResourceRequest, "ResourceType">,
attempt = 0
): Promise<AWS.SSM.RemoveTagsFromResourceResult> => {
try {
return await ssm.removeTagsFromResource({ ...params, ResourceType: "Parameter" }).promise();
} catch (error) {
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
await sleep();
// retry
return removeTagsFromParameter(ssm, params, attempt + 1);
}
throw error;
}
};
const deleteParametersBatch = async ( const deleteParametersBatch = async (
ssm: AWS.SSM, ssm: AWS.SSM,
parameters: AWS.SSM.Parameter[], parameters: AWS.SSM.Parameter[],
@@ -132,35 +293,92 @@ const deleteParametersBatch = async (
export const AwsParameterStoreSyncFns = { export const AwsParameterStoreSyncFns = {
syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => { syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => {
const { destinationConfig } = secretSync; const { destinationConfig, syncOptions } = secretSync;
const ssm = await getSSM(secretSync); const ssm = await getSSM(secretSync);
// TODO(scott): KMS Key ID, Tags
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path); const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
for await (const entry of Object.entries(secretMap)) { const awsParameterStoreMetadataRecord = await getParameterMetadataByPath(ssm, destinationConfig.path);
const [key, { value }] = entry;
// skip empty values (not allowed by AWS) or secrets that haven't changed const { shouldManageTags, awsParameterStoreTagsRecord } = await getParameterStoreTagsRecord(
if (!value || (key in awsParameterStoreSecretsRecord && awsParameterStoreSecretsRecord[key].Value === value)) { ssm,
awsParameterStoreSecretsRecord,
Boolean(syncOptions.tags?.length || syncOptions.syncSecretMetadataAsTags)
);
const syncTagsRecord = Object.fromEntries(syncOptions.tags?.map((tag) => [tag.key, tag.value]) ?? []);
for await (const entry of Object.entries(secretMap)) {
const [key, { value, secretMetadata }] = entry;
// skip empty values (not allowed by AWS)
if (!value) {
// eslint-disable-next-line no-continue // eslint-disable-next-line no-continue
continue; continue;
} }
try { const keyId = syncOptions.keyId ?? "alias/aws/ssm";
await putParameter(ssm, {
Name: `${destinationConfig.path}${key}`, // create parameter or update if changed
Type: "SecureString", if (
Value: value, !(key in awsParameterStoreSecretsRecord) ||
Overwrite: true value !== awsParameterStoreSecretsRecord[key].Value ||
}); keyId !== awsParameterStoreMetadataRecord[key]?.KeyId
} catch (error) { ) {
throw new SecretSyncError({ try {
error, await putParameter(ssm, {
secretKey: key Name: `${destinationConfig.path}${key}`,
Type: "SecureString",
Value: value,
Overwrite: true,
KeyId: keyId
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
if (shouldManageTags) {
const { tagsToAdd, tagKeysToRemove } = processParameterTags({
syncTagsRecord: {
// configured sync tags take preference over secret metadata
...(syncOptions.syncSecretMetadataAsTags &&
Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])),
...syncTagsRecord
},
awsTagsRecord: awsParameterStoreTagsRecord[key] ?? {}
}); });
if (tagsToAdd.length) {
try {
await addTagsToParameter(ssm, {
ResourceId: `${destinationConfig.path}${key}`,
Tags: tagsToAdd
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
if (tagKeysToRemove.length) {
try {
await removeTagsFromParameter(ssm, {
ResourceId: `${destinationConfig.path}${key}`,
TagKeys: tagKeysToRemove
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
} }
} }
@@ -8,6 +8,7 @@ import {
GenericCreateSecretSyncFieldsSchema, GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas"; } from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
const AwsParameterStoreSyncDestinationConfigSchema = z.object({ const AwsParameterStoreSyncDestinationConfigSchema = z.object({
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.region), region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.region),
@@ -20,19 +21,68 @@ const AwsParameterStoreSyncDestinationConfigSchema = z.object({
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.path) .describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.path)
}); });
export const AwsParameterStoreSyncSchema = BaseSecretSyncSchema(SecretSync.AWSParameterStore).extend({ const AwsParameterStoreSyncOptionsSchema = 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_PARAMETER_STORE.keyId),
tags: z
.object({
key: z
.string()
.regex(
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
"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 key cannot exceed 128 characters"),
value: z
.string()
.regex(
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
"Invalid resource tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
)
.max(256, "Resource tag value cannot exceed 256 characters")
})
.array()
.max(50)
.refine((items) => new Set(items.map((item) => item.key)).size === items.length, {
message: "Resource tag keys must be unique"
})
.optional()
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.tags),
syncSecretMetadataAsTags: z
.boolean()
.optional()
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.syncSecretMetadataAsTags)
});
const AwsParameterStoreSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
export const AwsParameterStoreSyncSchema = BaseSecretSyncSchema(
SecretSync.AWSParameterStore,
AwsParameterStoreSyncOptionsConfig,
AwsParameterStoreSyncOptionsSchema
).extend({
destination: z.literal(SecretSync.AWSParameterStore), destination: z.literal(SecretSync.AWSParameterStore),
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
}); });
export const CreateAwsParameterStoreSyncSchema = GenericCreateSecretSyncFieldsSchema( export const CreateAwsParameterStoreSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.AWSParameterStore SecretSync.AWSParameterStore,
AwsParameterStoreSyncOptionsConfig,
AwsParameterStoreSyncOptionsSchema
).extend({ ).extend({
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
}); });
export const UpdateAwsParameterStoreSyncSchema = GenericUpdateSecretSyncFieldsSchema( export const UpdateAwsParameterStoreSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.AWSParameterStore SecretSync.AWSParameterStore,
AwsParameterStoreSyncOptionsConfig,
AwsParameterStoreSyncOptionsSchema
).extend({ ).extend({
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema.optional() destinationConfig: AwsParameterStoreSyncDestinationConfigSchema.optional()
}); });
@@ -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,15 @@ 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]) ?? []);
const keyId = syncOptions.keyId ?? "alias/aws/secretsmanager";
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 +332,26 @@ 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 (awsValuesRecord[key]?.SecretString !== value || keyId !== awsDescriptionsRecord[key]?.KmsKeyId) {
// eslint-disable-next-line no-continue try {
continue; await updateSecret(client, {
SecretId: key,
SecretString: value,
KmsKeyId: 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: keyId
}); });
} catch (error) { } catch (error) {
throw new SecretSyncError({ throw new SecretSyncError({
@@ -227,12 +359,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,
@@ -261,17 +415,48 @@ export const AwsSecretsManagerSyncFns = {
Object.fromEntries(Object.entries(secretMap).map(([key, secretData]) => [key, secretData.value])) Object.fromEntries(Object.entries(secretMap).map(([key, secretData]) => [key, secretData.value]))
); );
if (awsValuesRecord[destinationConfig.secretName]) { if (awsSecretsRecord[destinationConfig.secretName]) {
await updateSecret(client, { await updateSecret(client, {
SecretId: destinationConfig.secretName, SecretId: destinationConfig.secretName,
SecretString: secretValue SecretString: secretValue,
KmsKeyId: keyId
}); });
} else { } else {
await createSecret(client, { await createSecret(client, {
Name: destinationConfig.secretName, Name: destinationConfig.secretName,
SecretString: secretValue SecretString: secretValue,
KmsKeyId: 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
});
}
}
} }
}, },
getSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials): Promise<TSecretMap> => { getSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials): Promise<TSecretMap> => {
@@ -9,6 +9,7 @@ import {
GenericCreateSecretSyncFieldsSchema, GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas"; } from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
const AwsSecretsManagerSyncDestinationConfigSchema = z const AwsSecretsManagerSyncDestinationConfigSchema = z
.discriminatedUnion("mappingBehavior", [ .discriminatedUnion("mappingBehavior", [
@@ -38,22 +39,95 @@ const AwsSecretsManagerSyncDestinationConfigSchema = z
}) })
); );
export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(SecretSync.AWSSecretsManager).extend({ 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,
AwsSecretsManagerSyncOptionsSchema
).extend({
destination: z.literal(SecretSync.AWSSecretsManager), destination: z.literal(SecretSync.AWSSecretsManager),
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
}); });
export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema( export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.AWSSecretsManager SecretSync.AWSSecretsManager,
).extend({ AwsSecretsManagerSyncOptionsConfig,
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema 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( export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.AWSSecretsManager SecretSync.AWSSecretsManager,
).extend({ AwsSecretsManagerSyncOptionsConfig,
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional() 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({ export const AwsSecretsManagerSyncListItemSchema = z.object({
name: z.literal("AWS Secrets Manager"), name: z.literal("AWS Secrets Manager"),
@@ -233,6 +233,7 @@ export const secretSyncQueueFactory = ({
} }
secretMap[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding); secretMap[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
secretMap[secretKey].secretMetadata = secret.secretMetadata;
}) })
); );
@@ -258,7 +259,8 @@ export const secretSyncQueueFactory = ({
secretMap[importedSecret.key] = { secretMap[importedSecret.key] = {
skipMultilineEncoding: importedSecret.skipMultilineEncoding, skipMultilineEncoding: importedSecret.skipMultilineEncoding,
comment: importedSecret.secretComment, comment: importedSecret.secretComment,
value: importedSecret.secretValue || "" value: importedSecret.secretValue || "",
secretMetadata: importedSecret.secretMetadata
}; };
} }
} }
@@ -1,4 +1,4 @@
import { z } from "zod"; import { AnyZodObject, z } from "zod";
import { SecretSyncsSchema } from "@app/db/schemas/secret-syncs"; import { SecretSyncsSchema } from "@app/db/schemas/secret-syncs";
import { SecretSyncs } from "@app/lib/api-docs"; import { SecretSyncs } from "@app/lib/api-docs";
@@ -8,34 +8,45 @@ import { SecretSync, SecretSyncInitialSyncBehavior } from "@app/services/secret-
import { SECRET_SYNC_CONNECTION_MAP } from "@app/services/secret-sync/secret-sync-maps"; import { SECRET_SYNC_CONNECTION_MAP } from "@app/services/secret-sync/secret-sync-maps";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
const SyncOptionsSchema = (secretSync: SecretSync, options: TSyncOptionsConfig = { canImportSecrets: true }) => const BaseSyncOptionsSchema = <T extends AnyZodObject | undefined = undefined>({
z.object({ destination,
initialSyncBehavior: (options.canImportSecrets syncOptionsConfig: { canImportSecrets },
merge,
isUpdateSchema
}: {
destination: SecretSync;
syncOptionsConfig: TSyncOptionsConfig;
merge?: T;
isUpdateSchema?: boolean;
}) => {
const baseSchema = z.object({
initialSyncBehavior: (canImportSecrets
? z.nativeEnum(SecretSyncInitialSyncBehavior) ? z.nativeEnum(SecretSyncInitialSyncBehavior)
: z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination) : z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination)
).describe(SecretSyncs.SYNC_OPTIONS(secretSync).initialSyncBehavior) ).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior)
// prependPrefix: z
// .string()
// .trim()
// .transform((str) => str.toUpperCase())
// .optional()
// .describe(SecretSyncs.SYNC_OPTIONS(secretSync).PREPEND_PREFIX),
// appendSuffix: z
// .string()
// .trim()
// .transform((str) => str.toUpperCase())
// .optional()
// .describe(SecretSyncs.SYNC_OPTIONS(secretSync).APPEND_SUFFIX)
}); });
export const BaseSecretSyncSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) => const schema = merge ? baseSchema.merge(merge) : baseSchema;
return (
isUpdateSchema
? schema.describe(SecretSyncs.UPDATE(destination).syncOptions).optional()
: schema.describe(SecretSyncs.CREATE(destination).syncOptions)
) as T extends AnyZodObject ? z.ZodObject<z.objectUtil.MergeShapes<typeof schema.shape, T["shape"]>> : typeof schema;
};
export const BaseSecretSyncSchema = <T extends AnyZodObject | undefined = undefined>(
destination: SecretSync,
syncOptionsConfig: TSyncOptionsConfig,
merge?: T
) =>
SecretSyncsSchema.omit({ SecretSyncsSchema.omit({
destination: true, destination: true,
destinationConfig: true, destinationConfig: true,
syncOptions: true syncOptions: true
}).extend({ }).extend({
// destination needs to be on the extended object for type differentiation // destination needs to be on the extended object for type differentiation
syncOptions: SyncOptionsSchema(destination, syncOptionsConfig), syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge }),
// join properties // join properties
projectId: z.string(), projectId: z.string(),
connection: z.object({ connection: z.object({
@@ -47,7 +58,11 @@ export const BaseSecretSyncSchema = (destination: SecretSync, syncOptionsConfig?
folder: z.object({ id: z.string(), path: z.string() }).nullable() folder: z.object({ id: z.string(), path: z.string() }).nullable()
}); });
export const GenericCreateSecretSyncFieldsSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) => export const GenericCreateSecretSyncFieldsSchema = <T extends AnyZodObject | undefined = undefined>(
destination: SecretSync,
syncOptionsConfig: TSyncOptionsConfig,
merge?: T
) =>
z.object({ z.object({
name: slugSchema({ field: "name" }).describe(SecretSyncs.CREATE(destination).name), name: slugSchema({ field: "name" }).describe(SecretSyncs.CREATE(destination).name),
projectId: z.string().trim().min(1, "Project ID required").describe(SecretSyncs.CREATE(destination).projectId), projectId: z.string().trim().min(1, "Project ID required").describe(SecretSyncs.CREATE(destination).projectId),
@@ -66,10 +81,14 @@ export const GenericCreateSecretSyncFieldsSchema = (destination: SecretSync, syn
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(SecretSyncs.CREATE(destination).secretPath), .describe(SecretSyncs.CREATE(destination).secretPath),
isAutoSyncEnabled: z.boolean().default(true).describe(SecretSyncs.CREATE(destination).isAutoSyncEnabled), isAutoSyncEnabled: z.boolean().default(true).describe(SecretSyncs.CREATE(destination).isAutoSyncEnabled),
syncOptions: SyncOptionsSchema(destination, syncOptionsConfig).describe(SecretSyncs.CREATE(destination).syncOptions) syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge })
}); });
export const GenericUpdateSecretSyncFieldsSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) => export const GenericUpdateSecretSyncFieldsSchema = <T extends AnyZodObject | undefined = undefined>(
destination: SecretSync,
syncOptionsConfig: TSyncOptionsConfig,
merge?: T
) =>
z.object({ z.object({
name: slugSchema({ field: "name" }).describe(SecretSyncs.UPDATE(destination).name).optional(), name: slugSchema({ field: "name" }).describe(SecretSyncs.UPDATE(destination).name).optional(),
connectionId: z.string().uuid().describe(SecretSyncs.UPDATE(destination).connectionId).optional(), connectionId: z.string().uuid().describe(SecretSyncs.UPDATE(destination).connectionId).optional(),
@@ -90,7 +109,5 @@ export const GenericUpdateSecretSyncFieldsSchema = (destination: SecretSync, syn
.optional() .optional()
.describe(SecretSyncs.UPDATE(destination).secretPath), .describe(SecretSyncs.UPDATE(destination).secretPath),
isAutoSyncEnabled: z.boolean().optional().describe(SecretSyncs.UPDATE(destination).isAutoSyncEnabled), isAutoSyncEnabled: z.boolean().optional().describe(SecretSyncs.UPDATE(destination).isAutoSyncEnabled),
syncOptions: SyncOptionsSchema(destination, syncOptionsConfig) syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge, isUpdateSchema: true })
.optional()
.describe(SecretSyncs.UPDATE(destination).syncOptions)
}); });
@@ -2,6 +2,7 @@ import { Job } from "bullmq";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { QueueJobs } from "@app/queue"; import { QueueJobs } from "@app/queue";
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
import { import {
TAwsSecretsManagerSync, TAwsSecretsManagerSync,
TAwsSecretsManagerSyncInput, TAwsSecretsManagerSyncInput,
@@ -197,5 +198,10 @@ export type TSendSecretSyncFailedNotificationsJobDTO = Job<
export type TSecretMap = Record< export type TSecretMap = Record<
string, string,
{ value: string; comment?: string; skipMultilineEncoding?: boolean | null | undefined } {
value: string;
comment?: string;
skipMultilineEncoding?: boolean | null | undefined;
secretMetadata?: ResourceMetadataDTO;
}
>; >;
Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 KiB

After

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 KiB

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 KiB

After

Width:  |  Height:  |  Size: 885 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 832 KiB

After

Width:  |  Height:  |  Size: 878 KiB

+54 -42
View File
@@ -82,22 +82,26 @@ Infisical supports two methods for connecting to AWS.
"Sid": "AllowSecretsManagerAccess", "Sid": "AllowSecretsManagerAccess",
"Effect": "Allow", "Effect": "Allow",
"Action": [ "Action": [
"secretsmanager:GetSecretValue", "secretsmanager:ListSecrets",
"secretsmanager:CreateSecret", "secretsmanager:GetSecretValue",
"secretsmanager:UpdateSecret", "secretsmanager:BatchGetSecretValue",
"secretsmanager:DescribeSecret", "secretsmanager:CreateSecret",
"secretsmanager:TagResource", "secretsmanager:UpdateSecret",
"secretsmanager:UntagResource", "secretsmanager:DeleteSecret",
"kms:ListKeys", "secretsmanager:DescribeSecret",
"kms:ListAliases", "secretsmanager:TagResource",
"kms:Encrypt", "secretsmanager:UntagResource",
"kms:Decrypt" "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
"kms:DescribeKey" // if you need to specify the KMS key
], ],
"Resource": "*" "Resource": "*"
} }
] ]
} }
``` ```
<Note>If using a custom KMS key, be sure to add the IAM user or role as a key user. ![KMS Key IAM Role User](/images/app-connections/aws/kms-key-user.png)</Note>
</Accordion> </Accordion>
<Accordion title="AWS Parameter Store"> <Accordion title="AWS Parameter Store">
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store: Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store:
@@ -112,23 +116,25 @@ Infisical supports two methods for connecting to AWS.
"Sid": "AllowSSMAccess", "Sid": "AllowSSMAccess",
"Effect": "Allow", "Effect": "Allow",
"Action": [ "Action": [
"ssm:PutParameter", "ssm:PutParameter",
"ssm:DeleteParameter", "ssm:GetParameters",
"ssm:GetParameters", "ssm:GetParametersByPath",
"ssm:GetParametersByPath", "ssm:DescribeParameters",
"ssm:DescribeParameters", "ssm:DeleteParameters",
"ssm:DeleteParameters", "ssm:ListTagsForResource", // if you need to add tags to secrets
"ssm:AddTagsToResource", // if you need to add tags to secrets "ssm:AddTagsToResource", // if you need to add tags to secrets
"kms:ListKeys", // if you need to specify the KMS key "ssm:RemoveTagsFromResource", // if you need to add tags to secrets
"kms:ListAliases", // 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:Encrypt", // if you need to specify the KMS key
"kms:Decrypt" // if you need to specify the KMS key "kms:Decrypt", // if you need to specify the KMS key
"kms:DescribeKey" // if you need to specify the KMS key
], ],
"Resource": "*" "Resource": "*"
} }
] ]
} }
``` ```
<Note>If using a custom KMS key, be sure to add the IAM user or role as a key user. ![KMS Key IAM Role User](/images/app-connections/aws/kms-key-user.png)</Note>
</Accordion> </Accordion>
</AccordionGroup> </AccordionGroup>
</Tab> </Tab>
@@ -223,22 +229,26 @@ Infisical supports two methods for connecting to AWS.
"Sid": "AllowSecretsManagerAccess", "Sid": "AllowSecretsManagerAccess",
"Effect": "Allow", "Effect": "Allow",
"Action": [ "Action": [
"secretsmanager:GetSecretValue", "secretsmanager:ListSecrets",
"secretsmanager:CreateSecret", "secretsmanager:GetSecretValue",
"secretsmanager:UpdateSecret", "secretsmanager:BatchGetSecretValue",
"secretsmanager:DescribeSecret", "secretsmanager:CreateSecret",
"secretsmanager:TagResource", "secretsmanager:UpdateSecret",
"secretsmanager:UntagResource", "secretsmanager:DeleteSecret",
"kms:ListKeys", "secretsmanager:DescribeSecret",
"kms:ListAliases", "secretsmanager:TagResource",
"kms:Encrypt", "secretsmanager:UntagResource",
"kms:Decrypt" "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
"kms:DescribeKey" // if you need to specify the KMS key
], ],
"Resource": "*" "Resource": "*"
} }
] ]
} }
``` ```
<Note>If using a custom KMS key, be sure to add the IAM user or role as a key user. ![KMS Key IAM Role User](/images/app-connections/aws/kms-key-user.png)</Note>
</Accordion> </Accordion>
<Accordion title="AWS Parameter Store"> <Accordion title="AWS Parameter Store">
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store: Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store:
@@ -253,23 +263,25 @@ Infisical supports two methods for connecting to AWS.
"Sid": "AllowSSMAccess", "Sid": "AllowSSMAccess",
"Effect": "Allow", "Effect": "Allow",
"Action": [ "Action": [
"ssm:PutParameter", "ssm:PutParameter",
"ssm:DeleteParameter", "ssm:GetParameters",
"ssm:GetParameters", "ssm:GetParametersByPath",
"ssm:GetParametersByPath", "ssm:DescribeParameters",
"ssm:DescribeParameters", "ssm:DeleteParameters",
"ssm:DeleteParameters", "ssm:ListTagsForResource", // if you need to add tags to secrets
"ssm:AddTagsToResource", // if you need to add tags to secrets "ssm:AddTagsToResource", // if you need to add tags to secrets
"kms:ListKeys", // if you need to specify the KMS key "ssm:RemoveTagsFromResource", // if you need to add tags to secrets
"kms:ListAliases", // 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:Encrypt", // if you need to specify the KMS key
"kms:Decrypt" // if you need to specify the KMS key "kms:Decrypt", // if you need to specify the KMS key
"kms:DescribeKey" // if you need to specify the KMS key
], ],
"Resource": "*" "Resource": "*"
} }
] ]
} }
``` ```
<Note>If using a custom KMS key, be sure to add the IAM user or role as a key user. ![KMS Key IAM Role User](/images/app-connections/aws/kms-key-user.png)</Note>
</Accordion> </Accordion>
</AccordionGroup> </AccordionGroup>
</Tab> </Tab>
@@ -40,6 +40,10 @@ 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.
- **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>
- **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 Parameter Store Sync, then click **Next**. 6. Configure the **Details** of your Parameter Store Sync, then click **Next**.
@@ -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**.
@@ -77,6 +77,13 @@ via the UI or API for the third-party service you intend to sync secrets to.
- <strong>Destination:</strong> The App Connection to utilize and the destination endpoint to deploy secrets to. These can vary between services. - <strong>Destination:</strong> The App Connection to utilize and the destination endpoint to deploy secrets to. These can vary between services.
- <strong>Options:</strong> Customize how secrets should be synced. Examples include adding a suffix or prefix to your secrets, or importing secrets from the destination on the initial sync. - <strong>Options:</strong> Customize how secrets should be synced. Examples include adding a suffix or prefix to your secrets, or importing secrets from the destination on the initial sync.
<Note>
Secret Syncs are the source of truth for connected third-party services. Any secret,
including associated data, not present or imported in Infisical before syncing will be
overwritten, and changes made directly in the connected service outside of infisical may also
be overwritten by future syncs.
</Note>
<Info> <Info>
Some third-party services do not support importing secrets. Some third-party services do not support importing secrets.
</Info> </Info>
@@ -1,11 +1,13 @@
import { useState } from "react"; import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form"; import { Controller, FormProvider, useForm } from "react-hook-form";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, Checkbox, FormControl, Switch } from "@app/components/v2"; import { Button, FormControl, Switch } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { import {
@@ -16,10 +18,10 @@ import {
useSecretSyncOption useSecretSyncOption
} from "@app/hooks/api/secretSyncs"; } from "@app/hooks/api/secretSyncs";
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields/SecretSyncOptionsFields";
import { SecretSyncFormSchema, TSecretSyncForm } from "./schemas"; import { SecretSyncFormSchema, TSecretSyncForm } from "./schemas";
import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields"; import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields";
import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields"; import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields";
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields";
import { SecretSyncReviewFields } from "./SecretSyncReviewFields"; import { SecretSyncReviewFields } from "./SecretSyncReviewFields";
import { SecretSyncSourceFields } from "./SecretSyncSourceFields"; import { SecretSyncSourceFields } from "./SecretSyncSourceFields";
@@ -32,7 +34,7 @@ type Props = {
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretSyncForm)[] }[] = [ const FORM_TABS: { name: string; key: string; fields: (keyof TSecretSyncForm)[] }[] = [
{ name: "Source", key: "source", fields: ["secretPath", "environment"] }, { name: "Source", key: "source", fields: ["secretPath", "environment"] },
{ name: "Destination", key: "destination", fields: ["connection", "destinationConfig"] }, { name: "Destination", key: "destination", fields: ["connection", "destinationConfig"] },
{ name: "Options", key: "options", fields: ["syncOptions"] }, { name: "Sync Options", key: "options", fields: ["syncOptions"] },
{ name: "Details", key: "details", fields: ["name", "description"] }, { name: "Details", key: "details", fields: ["name", "description"] },
{ name: "Review", key: "review", fields: [] } { name: "Review", key: "review", fields: [] }
]; ];
@@ -42,8 +44,9 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { name: destinationName } = SECRET_SYNC_MAP[destination]; const { name: destinationName } = SECRET_SYNC_MAP[destination];
const [showConfirmation, setShowConfirmation] = useState(false);
const [selectedTabIndex, setSelectedTabIndex] = useState(0); const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const [confirmOverwrite, setConfirmOverwrite] = useState(false);
const { syncOption } = useSecretSyncOption(destination); const { syncOption } = useSecretSyncOption(destination);
@@ -77,6 +80,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
onComplete(secretSync); onComplete(secretSync);
} catch (err: any) { } catch (err: any) {
console.error(err); console.error(err);
setShowConfirmation(false);
createNotification({ createNotification({
title: `Failed to add ${destinationName} Sync`, title: `Failed to add ${destinationName} Sync`,
text: err.message, text: err.message,
@@ -94,7 +98,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
setSelectedTabIndex((prev) => prev - 1); setSelectedTabIndex((prev) => prev - 1);
}; };
const { handleSubmit, trigger, watch, control } = formMethods; const { handleSubmit, trigger, control } = formMethods;
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields); const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
@@ -102,7 +106,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
const handleNext = async () => { const handleNext = async () => {
if (isFinalStep) { if (isFinalStep) {
handleSubmit(onSubmit)(); setShowConfirmation(true);
return; return;
} }
@@ -123,7 +127,42 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
return isEnabled; return isEnabled;
}; };
const initialSyncBehavior = watch("syncOptions.initialSyncBehavior"); if (showConfirmation)
return (
<>
<div className="flex flex-col rounded-sm border border-l-[2px] border-mineshaft-600 border-l-primary bg-mineshaft-700/80 px-4 py-3">
<div className="mb-1 flex items-center text-sm">
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1.5 text-primary" />
Secret Sync Behavior
</div>
<p className="mt-1 text-sm text-bunker-200">
Secret Syncs are the source of truth for connected third-party services. Any secret,
including associated data, not present or imported in Infisical before syncing will be
overwritten, and changes made directly in the connected service outside of infisical may
also be overwritten by future syncs.
</p>
</div>
<div className="mt-4 flex gap-4">
<Button
isDisabled={createSecretSync.isPending}
isLoading={createSecretSync.isPending}
onClick={handleSubmit(onSubmit)}
colorSchema="secondary"
>
I Understand
</Button>
<Button
isDisabled={createSecretSync.isPending}
variant="plain"
onClick={() => setShowConfirmation(false)}
colorSchema="secondary"
>
Cancel
</Button>
</div>
</>
);
return ( return (
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}> <form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}>
@@ -174,7 +213,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}
@@ -196,32 +235,9 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
</Tab.Panels> </Tab.Panels>
</Tab.Group> </Tab.Group>
</FormProvider> </FormProvider>
{isFinalStep &&
initialSyncBehavior === SecretSyncInitialSyncBehavior.OverwriteDestination && (
<Checkbox
id="confirm-overwrite"
isChecked={confirmOverwrite}
containerClassName="-mt-5"
onCheckedChange={(isChecked) => setConfirmOverwrite(Boolean(isChecked))}
>
<p
className={`mt-5 text-wrap text-xs ${confirmOverwrite ? "text-mineshaft-200" : "text-red"}`}
>
I understand all secrets present in the configured {destinationName} destination will
be removed if they are not present within Infisical.
</p>
</Checkbox>
)}
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4"> <div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
<Button <Button onClick={handleNext} colorSchema="secondary">
isDisabled={
isFinalStep &&
initialSyncBehavior === SecretSyncInitialSyncBehavior.OverwriteDestination &&
!confirmOverwrite
}
onClick={handleNext}
colorSchema="secondary"
>
{isFinalStep ? "Create Sync" : "Next"} {isFinalStep ? "Create Sync" : "Next"}
</Button> </Button>
{selectedTabIndex > 0 && ( {selectedTabIndex > 0 && (
@@ -8,10 +8,10 @@ import { Button, ModalClose } from "@app/components/v2";
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { TSecretSync, useUpdateSecretSync } from "@app/hooks/api/secretSyncs"; import { TSecretSync, useUpdateSecretSync } from "@app/hooks/api/secretSyncs";
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields/SecretSyncOptionsFields";
import { TSecretSyncForm, UpdateSecretSyncFormSchema } from "./schemas"; import { TSecretSyncForm, UpdateSecretSyncFormSchema } from "./schemas";
import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields"; import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields";
import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields"; import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields";
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields";
import { SecretSyncSourceFields } from "./SecretSyncSourceFields"; import { SecretSyncSourceFields } from "./SecretSyncSourceFields";
type Props = { type Props = {
@@ -8,13 +8,17 @@ import { TSecretSyncForm } from "../schemas";
import { AwsRegionSelect } from "./shared"; import { AwsRegionSelect } from "./shared";
export const AwsParameterStoreSyncFields = () => { export const AwsParameterStoreSyncFields = () => {
const { control } = useFormContext< const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSParameterStore } TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
>(); >();
return ( return (
<> <>
<SecretSyncConnectionField /> <SecretSyncConnectionField
onChange={() => {
setValue("syncOptions.keyId", undefined);
}}
/>
<Controller <Controller
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Region"> <FormControl isError={Boolean(error)} errorText={error?.message} label="Region">
@@ -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,209 @@
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 { TSecretSyncForm } from "../schemas";
export const AwsParameterStoreSyncOptionsFields = () => {
const { control, watch } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
>();
const region = watch("destinationConfig.region");
const connectionId = useWatch({ name: "connection.id", control });
const { data: kmsKeys = [], isPending: isKmsKeysPending } = useListAwsConnectionKmsKeys(
{
connectionId,
region,
destination: SecretSync.AWSParameterStore
},
{ 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 parameters with"
isError={Boolean(error)}
errorText={error?.message}
label="KMS Key"
>
<FilterableSelect
isLoading={isKmsKeysPending && Boolean(connectionId && region)}
isDisabled={!connectionId}
value={kmsKeys.find((org) => org.alias === value) ?? null}
onChange={(option) =>
onChange((option as SingleValue<TAwsConnectionKmsKey>)?.alias ?? null)
}
// eslint-disable-next-line react/no-unstable-nested-components
noOptionsMessage={({ inputValue }) =>
inputValue ? undefined : (
<p>
To configure a KMS key, ensure the following permissions are present on the
selected IAM role:{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:ListAliases&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:DescribeKey&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:Encrypt&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:Decrypt&#34;
</span>
.
</p>
)
}
options={kmsKeys}
placeholder="Leave blank to use default KMS key"
getOptionLabel={(option) =>
option.alias === "alias/aws/ssm" ? `${option.alias} (Default)` : option.alias
}
getOptionValue={(option) => option.alias}
/>
</FormControl>
)}
/>
<FormLabel
label="Resource Tags"
tooltipText="Add resource tags to parameters 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="mt-2 flex">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => tagFields.append({ key: "", value: "" })}
>
Add Tag
</Button>
</div>
<Controller
name="syncOptions.syncSecretMetadataAsTags"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
className="mt-6"
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-[18rem]">
Sync Secret Metadata as Resource Tags{" "}
<Tooltip
className="max-w-md"
content={
<>
<p>
If enabled, metadata attached to secrets will be added as resource tags to
parameters 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>
)}
/>
</>
);
};
@@ -0,0 +1,208 @@
import { Fragment } from "react";
import { Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form";
import { SingleValue } from "react-select";
import { faPlus, faQuestionCircle, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
FilterableSelect,
FormControl,
FormLabel,
IconButton,
Input,
Switch,
Tooltip
} from "@app/components/v2";
import {
TAwsConnectionKmsKey,
useListAwsConnectionKmsKeys
} from "@app/hooks/api/appConnections/aws";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
import { TSecretSyncForm } from "../schemas";
export const AwsSecretsManagerSyncOptionsFields = () => {
const { control, watch } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
>();
const region = watch("destinationConfig.region");
const connectionId = useWatch({ name: "connection.id", control });
const mappingBehavior = watch("destinationConfig.mappingBehavior");
const { data: kmsKeys = [], isPending: isKmsKeysPending } = useListAwsConnectionKmsKeys(
{
connectionId,
region,
destination: SecretSync.AWSSecretsManager
},
{ enabled: Boolean(connectionId && region) }
);
const tagFields = useFieldArray({
control,
name: "syncOptions.tags"
});
return (
<>
<Controller
name="syncOptions.keyId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText="The AWS KMS key to encrypt secrets with"
isError={Boolean(error)}
errorText={error?.message}
label="KMS Key"
>
<FilterableSelect
isLoading={isKmsKeysPending && Boolean(connectionId && region)}
isDisabled={!connectionId}
value={kmsKeys.find((org) => org.alias === value) ?? null}
onChange={(option) =>
onChange((option as SingleValue<TAwsConnectionKmsKey>)?.alias ?? null)
}
// eslint-disable-next-line react/no-unstable-nested-components
noOptionsMessage={({ inputValue }) =>
inputValue ? undefined : (
<p>
To configure a KMS key, ensure the following permissions are present on the
selected IAM role:{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:ListAliases&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:DescribeKey&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:Encrypt&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:Decrypt&#34;
</span>
.
</p>
)
}
options={kmsKeys}
placeholder="Leave blank to use default KMS key"
getOptionLabel={(option) =>
option.alias === "alias/aws/secretsmanager"
? `${option.alias} (Default)`
: option.alias
}
getOptionValue={(option) => option.alias}
/>
</FormControl>
)}
/>
<FormLabel label="Tags" tooltipText="Add tags to secrets synced by Infisical" />
<div className="mb-3 grid max-h-[20vh] grid-cols-12 flex-col items-end gap-2 overflow-y-auto">
{tagFields.fields.map(({ id: tagFieldId }, i) => (
<Fragment key={tagFieldId}>
<div className="col-span-5">
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
<Controller
control={control}
name={`syncOptions.tags.${i}.key`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input className="text-xs" {...field} />
</FormControl>
)}
/>
</div>
<div className="col-span-6">
{i === 0 && (
<FormLabel label="Value" className="text-xs text-mineshaft-400" isOptional />
)}
<Controller
control={control}
name={`syncOptions.tags.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input className="text-xs" {...field} />
</FormControl>
)}
/>
</div>
<Tooltip content="Remove tag" position="right">
<IconButton
variant="plain"
ariaLabel="Remove tag"
className="col-span-1 mb-1.5"
colorSchema="danger"
size="xs"
onClick={() => tagFields.remove(i)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
</Fragment>
))}
</div>
<div className="mb-6 mt-2 flex">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => tagFields.append({ key: "", value: "" })}
>
Add Tag
</Button>
</div>
{mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne && (
<Controller
name="syncOptions.syncSecretMetadataAsTags"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error?.message)} errorText={error?.message}>
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="overwrite-existing-secrets"
thumbClassName="bg-mineshaft-800"
isChecked={value}
onCheckedChange={onChange}
>
<p className="w-[14rem]">
Sync Secret Metadata as Tags{" "}
<Tooltip
className="max-w-md"
content={
<>
<p>
If enabled, metadata attached to secrets will be added as tags to secrets
synced by Infisical.
</p>
<p className="mt-4">
Manually configured tags from the field above will take precedence over
secret metadata when tag keys conflict.
</p>
</>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</p>
</Switch>
</FormControl>
)}
/>
)}
</>
);
};
@@ -1,12 +1,15 @@
import { ReactNode } from "react";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons"; import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FormControl, Select, SelectItem } from "@app/components/v2"; import { FormControl, Select, SelectItem } from "@app/components/v2";
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { useSecretSyncOption } from "@app/hooks/api/secretSyncs"; import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "./schemas"; import { TSecretSyncForm } from "../schemas";
import { AwsParameterStoreSyncOptionsFields } from "./AwsParameterStoreSyncOptionsFields";
import { AwsSecretsManagerSyncOptionsFields } from "./AwsSecretsManagerSyncOptionsFields";
type Props = { type Props = {
hideInitialSync?: boolean; hideInitialSync?: boolean;
@@ -21,6 +24,26 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
const { syncOption } = useSecretSyncOption(destination); const { syncOption } = useSecretSyncOption(destination);
let AdditionalSyncOptionsFieldsComponent: ReactNode;
switch (destination) {
case SecretSync.AWSParameterStore:
AdditionalSyncOptionsFieldsComponent = <AwsParameterStoreSyncOptionsFields />;
break;
case SecretSync.AWSSecretsManager:
AdditionalSyncOptionsFieldsComponent = <AwsSecretsManagerSyncOptionsFields />;
break;
case SecretSync.GitHub:
case SecretSync.GCPSecretManager:
case SecretSync.AzureKeyVault:
case SecretSync.AzureAppConfiguration:
case SecretSync.Databricks:
AdditionalSyncOptionsFieldsComponent = null;
break;
default:
throw new Error(`Unhandled Additional Sync Options Fields: ${destination}`);
}
return ( return (
<> <>
<p className="mb-4 text-sm text-bunker-300">Configure how secrets should be synced.</p> <p className="mb-4 text-sm text-bunker-300">Configure how secrets should be synced.</p>
@@ -91,6 +114,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
)} )}
</> </>
)} )}
{AdditionalSyncOptionsFieldsComponent}
{/* <Controller {/* <Controller
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl <FormControl
@@ -0,0 +1 @@
export * from "./SecretSyncOptionsFields";
@@ -1,17 +1,71 @@
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";
export const AwsParameterStoreSyncReviewFields = () => { export const AwsParameterStoreSyncOptionsReviewFields = () => {
const { watch } = useFormContext< const { watch } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSParameterStore } TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
>(); >();
const [region, path] = watch(["destinationConfig.region", "destinationConfig.path"]); const [{ keyId, tags, syncSecretMetadataAsTags }] = watch(["syncOptions"]);
return (
<>
{keyId && <SecretSyncLabel label="KMS Key">{keyId}</SecretSyncLabel>}
{tags && tags.length > 0 && (
<SecretSyncLabel label="Resource 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>
)}
</>
);
};
export const AwsParameterStoreDestinationReviewFields = () => {
const { watch } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
>();
const [{ region, path }] = watch(["destinationConfig"]);
const awsRegion = AWS_REGIONS.find((r) => r.slug === region); const awsRegion = AWS_REGIONS.find((r) => r.slug === region);
@@ -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>
)}
</>
);
};
@@ -3,15 +3,21 @@ import { useFormContext } from "react-hook-form";
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 { AwsSecretsManagerSyncReviewFields } from "@app/components/secret-syncs/forms/SecretSyncReviewFields/AwsSecretsManagerSyncReviewFields";
import { DatabricksSyncReviewFields } from "@app/components/secret-syncs/forms/SecretSyncReviewFields/DatabricksSyncReviewFields";
import { Badge } from "@app/components/v2"; import { Badge } from "@app/components/v2";
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { SecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs";
import { AwsParameterStoreSyncReviewFields } from "./AwsParameterStoreSyncReviewFields"; import {
AwsParameterStoreDestinationReviewFields,
AwsParameterStoreSyncOptionsReviewFields
} from "./AwsParameterStoreSyncReviewFields";
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 { GcpSyncReviewFields } from "./GcpSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields"; import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
@@ -19,6 +25,7 @@ export const SecretSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm>(); const { watch } = useFormContext<TSecretSyncForm>();
let DestinationFieldsComponent: ReactNode; let DestinationFieldsComponent: ReactNode;
let AdditionalSyncOptionsFieldsComponent: ReactNode;
const { const {
name, name,
@@ -38,10 +45,12 @@ export const SecretSyncReviewFields = () => {
switch (destination) { switch (destination) {
case SecretSync.AWSParameterStore: case SecretSync.AWSParameterStore:
DestinationFieldsComponent = <AwsParameterStoreSyncReviewFields />; DestinationFieldsComponent = <AwsParameterStoreDestinationReviewFields />;
AdditionalSyncOptionsFieldsComponent = <AwsParameterStoreSyncOptionsReviewFields />;
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 />;
@@ -84,7 +93,7 @@ export const SecretSyncReviewFields = () => {
</div> </div>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="w-full border-b border-mineshaft-600"> <div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-300">Options</span> <span className="text-sm text-mineshaft-300">Sync Options</span>
</div> </div>
<div className="flex flex-wrap gap-x-8 gap-y-2"> <div className="flex flex-wrap gap-x-8 gap-y-2">
<SecretSyncLabel label="Auto-Sync"> <SecretSyncLabel label="Auto-Sync">
@@ -97,6 +106,7 @@ export const SecretSyncReviewFields = () => {
</SecretSyncLabel> </SecretSyncLabel>
{/* <SecretSyncLabel label="Prepend Prefix">{prependPrefix}</SecretSyncLabel> {/* <SecretSyncLabel label="Prepend Prefix">{prependPrefix}</SecretSyncLabel>
<SecretSyncLabel label="Append Suffix">{appendSuffix}</SecretSyncLabel> */} <SecretSyncLabel label="Append Suffix">{appendSuffix}</SecretSyncLabel> */}
{AdditionalSyncOptionsFieldsComponent}
</div> </div>
</div> </div>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
@@ -1,16 +1,45 @@
import { z } from "zod"; import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs";
export const AwsParameterStoreSyncDestinationSchema = z.object({ export const AwsParameterStoreSyncDestinationSchema = BaseSecretSyncSchema(
destination: z.literal(SecretSync.AWSParameterStore), z.object({
destinationConfig: z.object({ keyId: z.string().optional(),
path: z tags: z
.string() .object({
.trim() key: z
.min(1, "Parameter Store Path required") .string()
.max(2048, "Cannot exceed 2048 characters") .regex(
.regex(/^\/([/]|(([\w-]+\/)+))?$/, 'Invalid path - must follow "/example/path/" format'), /^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
region: z.string().min(1, "Region required") "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.AWSParameterStore),
destinationConfig: z.object({
path: z
.string()
.trim()
.min(1, "Parameter Store Path required")
.max(2048, "Cannot exceed 2048 characters")
.regex(/^\/([/]|(([\w-]+\/)+))?$/, 'Invalid path - must follow "/example/path/" format'),
region: z.string().min(1, "Region required")
})
})
);
@@ -1,30 +1,59 @@
import { z } from "zod"; import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
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 = z.object({ export const AwsSecretsManagerSyncDestinationSchema = BaseSecretSyncSchema(
destination: z.literal(SecretSync.AWSSecretsManager), z.object({
destinationConfig: z keyId: z.string().optional(),
.discriminatedUnion("mappingBehavior", [ tags: z
z.object({ .object({
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.OneToOne) key: z
}),
z.object({
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne),
secretName: z
.string() .string()
.regex( .regex(
/^[a-zA-Z0-9/_+=.@-]+$/, /^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
"Secret name must contain only alphanumeric characters and the characters /_+=.@-" "Keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
) )
.min(1, "Secret name is required") .min(1, "Key required")
.max(256, "Secret name cannot exceed 256 characters") .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()
.and( .max(50)
z.object({ .optional(),
region: z.string().min(1, "Region required") syncSecretMetadataAsTags: z.boolean().optional()
}) })
) ).merge(
}); z.object({
destination: z.literal(SecretSync.AWSSecretsManager),
destinationConfig: z
.discriminatedUnion("mappingBehavior", [
z.object({
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.OneToOne)
}),
z.object({
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne),
secretName: z
.string()
.regex(
/^[a-zA-Z0-9/_+=.@-]+$/,
"Secret name must contain only alphanumeric characters and the characters /_+=.@-"
)
.min(1, "Secret name is required")
.max(256, "Secret name cannot exceed 256 characters")
})
])
.and(
z.object({
region: z.string().min(1, "Region required")
})
)
})
);
@@ -1,19 +1,22 @@
import { z } from "zod"; import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs";
export const AzureAppConfigurationSyncDestinationSchema = z.object({ export const AzureAppConfigurationSyncDestinationSchema = BaseSecretSyncSchema().merge(
destination: z.literal(SecretSync.AzureAppConfiguration), z.object({
destinationConfig: z.object({ destination: z.literal(SecretSync.AzureAppConfiguration),
configurationUrl: z destinationConfig: z.object({
.string() configurationUrl: z
.trim() .string()
.min(1, { message: "Azure App Configuration URL is required" }) .trim()
.url() .min(1, { message: "Azure App Configuration URL is required" })
.refine( .url()
(val) => val.endsWith(".azconfig.io"), .refine(
"URL should have the following format: https://resource-name-here.azconfig.io" (val) => val.endsWith(".azconfig.io"),
), "URL should have the following format: https://resource-name-here.azconfig.io"
label: z.string().optional() ),
label: z.string().optional()
})
}) })
}); );
@@ -1,10 +1,16 @@
import { z } from "zod"; import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs";
export const AzureKeyVaultSyncDestinationSchema = z.object({ export const AzureKeyVaultSyncDestinationSchema = BaseSecretSyncSchema().merge(
destination: z.literal(SecretSync.AzureKeyVault), z.object({
destinationConfig: z.object({ destination: z.literal(SecretSync.AzureKeyVault),
vaultBaseUrl: z.string().url("Invalid vault base URL format").min(1, "Vault base URL required") destinationConfig: z.object({
vaultBaseUrl: z
.string()
.url("Invalid vault base URL format")
.min(1, "Vault base URL required")
})
}) })
}); );
@@ -0,0 +1,39 @@
import { AnyZodObject, z } from "zod";
import { SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs";
import { slugSchema } from "@app/lib/schemas";
export const BaseSecretSyncSchema = <T extends AnyZodObject | undefined = undefined>(
additionalSyncOptions?: T
) => {
const baseSyncOptionsSchema = z.object({
initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior)
// scott: removed temporarily for evaluation of template formatting
// prependPrefix: z
// .string()
// .trim()
// .transform((str) => str.toUpperCase())
// .optional(),
// appendSuffix: z
// .string()
// .trim()
// .transform((str) => str.toUpperCase())
// .optional()
});
const syncOptionsSchema = additionalSyncOptions
? baseSyncOptionsSchema.merge(additionalSyncOptions)
: (baseSyncOptionsSchema as T extends AnyZodObject
? z.ZodObject<z.objectUtil.MergeShapes<typeof baseSyncOptionsSchema.shape, T["shape"]>>
: typeof baseSyncOptionsSchema);
return z.object({
name: slugSchema({ field: "Name" }),
description: z.string().trim().max(256, "Cannot exceed 256 characters").optional(),
connection: z.object({ name: z.string(), id: z.string().uuid() }),
environment: z.object({ slug: z.string(), id: z.string(), name: z.string() }),
secretPath: z.string().min(1, "Secret path required"),
syncOptions: syncOptionsSchema,
isAutoSyncEnabled: z.boolean()
});
};
@@ -1,10 +1,13 @@
import { z } from "zod"; import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs";
export const DatabricksSyncDestinationSchema = z.object({ export const DatabricksSyncDestinationSchema = BaseSecretSyncSchema().merge(
destination: z.literal(SecretSync.Databricks), z.object({
destinationConfig: z.object({ destination: z.literal(SecretSync.Databricks),
scope: z.string().trim().min(1, "Databricks scope required") destinationConfig: z.object({
scope: z.string().trim().min(1, "Databricks scope required")
})
}) })
}); );
@@ -1,12 +1,15 @@
import { z } from "zod"; import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs";
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync"; import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
export const GcpSyncDestinationSchema = z.object({ export const GcpSyncDestinationSchema = BaseSecretSyncSchema().merge(
destination: z.literal(SecretSync.GCPSecretManager), z.object({
destinationConfig: z.object({ destination: z.literal(SecretSync.GCPSecretManager),
scope: z.literal(GcpSyncScope.Global), destinationConfig: z.object({
projectId: z.string().min(1, "Project ID required") scope: z.literal(GcpSyncScope.Global),
projectId: z.string().min(1, "Project ID required")
})
}) })
}); );
@@ -1,45 +1,48 @@
import { z } from "zod"; import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs";
import { import {
GitHubSyncScope, GitHubSyncScope,
GitHubSyncVisibility GitHubSyncVisibility
} from "@app/hooks/api/secretSyncs/types/github-sync"; } from "@app/hooks/api/secretSyncs/types/github-sync";
export const GitHubSyncDestinationSchema = z.object({ export const GitHubSyncDestinationSchema = BaseSecretSyncSchema().merge(
destination: z.literal(SecretSync.GitHub), z.object({
destinationConfig: z destination: z.literal(SecretSync.GitHub),
.discriminatedUnion("scope", [ destinationConfig: z
z.object({ .discriminatedUnion("scope", [
scope: z.literal(GitHubSyncScope.Organization), z.object({
org: z.string().min(1, "Organization name required"), scope: z.literal(GitHubSyncScope.Organization),
visibility: z.nativeEnum(GitHubSyncVisibility), org: z.string().min(1, "Organization name required"),
selectedRepositoryIds: z.number().array().optional() visibility: z.nativeEnum(GitHubSyncVisibility),
}), selectedRepositoryIds: z.number().array().optional()
z.object({ }),
scope: z.literal(GitHubSyncScope.Repository), z.object({
owner: z.string().min(1, "Repository owner name required"), scope: z.literal(GitHubSyncScope.Repository),
repo: z.string().min(1, "Repository name required") owner: z.string().min(1, "Repository owner name required"),
}), repo: z.string().min(1, "Repository name required")
z.object({ }),
scope: z.literal(GitHubSyncScope.RepositoryEnvironment), z.object({
owner: z.string().min(1, "Repository owner name required"), scope: z.literal(GitHubSyncScope.RepositoryEnvironment),
repo: z.string().min(1, "Repository name required"), owner: z.string().min(1, "Repository owner name required"),
env: z.string().min(1, "Environment name required") repo: z.string().min(1, "Repository name required"),
}) env: z.string().min(1, "Environment name required")
]) })
.superRefine((options, ctx) => { ])
if (options.scope === GitHubSyncScope.Organization) { .superRefine((options, ctx) => {
if ( if (options.scope === GitHubSyncScope.Organization) {
options.visibility === GitHubSyncVisibility.Selected && if (
!options.selectedRepositoryIds?.length options.visibility === GitHubSyncVisibility.Selected &&
) { !options.selectedRepositoryIds?.length
ctx.addIssue({ ) {
code: z.ZodIssueCode.custom, ctx.addIssue({
message: "Select at least 1 repository", code: z.ZodIssueCode.custom,
path: ["selectedRepositoryIds"] message: "Select at least 1 repository",
}); path: ["selectedRepositoryIds"]
});
}
} }
} })
}) })
}); );
@@ -3,37 +3,12 @@ import { z } from "zod";
import { AwsSecretsManagerSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema"; import { AwsSecretsManagerSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema";
import { DatabricksSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/databricks-sync-destination-schema"; import { DatabricksSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/databricks-sync-destination-schema";
import { GitHubSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/github-sync-destination-schema"; import { GitHubSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/github-sync-destination-schema";
import { SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs";
import { slugSchema } from "@app/lib/schemas";
import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema"; import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema"; import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema";
import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema"; import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema";
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
const BaseSecretSyncSchema = z.object({
name: slugSchema({ field: "Name" }),
description: z.string().trim().max(256, "Cannot exceed 256 characters").optional(),
connection: z.object({ name: z.string(), id: z.string().uuid() }),
environment: z.object({ slug: z.string(), id: z.string(), name: z.string() }),
secretPath: z.string().min(1, "Secret path required"),
syncOptions: z.object({
initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior)
// scott: removed temporarily for evaluation of template formatting
// prependPrefix: z
// .string()
// .trim()
// .transform((str) => str.toUpperCase())
// .optional(),
// appendSuffix: z
// .string()
// .trim()
// .transform((str) => str.toUpperCase())
// .optional()
}),
isAutoSyncEnabled: z.boolean()
});
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncDestinationSchema, AwsParameterStoreSyncDestinationSchema,
AwsSecretsManagerSyncDestinationSchema, AwsSecretsManagerSyncDestinationSchema,
@@ -44,8 +19,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
DatabricksSyncDestinationSchema DatabricksSyncDestinationSchema
]); ]);
export const SecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema); export const SecretSyncFormSchema = SecretSyncUnionSchema;
export const UpdateSecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema.partial()); export const UpdateSecretSyncFormSchema = SecretSyncUnionSchema;
export type TSecretSyncForm = z.infer<typeof SecretSyncFormSchema>; export type TSecretSyncForm = z.infer<typeof SecretSyncFormSchema>;
@@ -0,0 +1,2 @@
export * from "./queries";
export * from "./types";
@@ -0,0 +1,42 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "@app/hooks/api/appConnections";
import {
TAwsConnectionKmsKey,
TAwsConnectionListKmsKeysResponse,
TListAwsConnectionKmsKeys
} from "./types";
const awsConnectionKeys = {
all: [...appConnectionKeys.all, "aws"] as const,
listKmsKeys: (params: TListAwsConnectionKmsKeys) =>
[...awsConnectionKeys.all, "kms-keys", params] as const
};
export const useListAwsConnectionKmsKeys = (
{ connectionId, ...params }: TListAwsConnectionKmsKeys,
options?: Omit<
UseQueryOptions<
TAwsConnectionKmsKey[],
unknown,
TAwsConnectionKmsKey[],
ReturnType<typeof awsConnectionKeys.listKmsKeys>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: awsConnectionKeys.listKmsKeys({ connectionId, ...params }),
queryFn: async () => {
const { data } = await apiRequest.get<TAwsConnectionListKmsKeysResponse>(
`/api/v1/app-connections/aws/${connectionId}/kms-keys`,
{ params }
);
return data.kmsKeys;
},
...options
});
};
@@ -0,0 +1,16 @@
import { SecretSync } from "@app/hooks/api/secretSyncs";
export type TListAwsConnectionKmsKeys = {
connectionId: string;
region: string;
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
};
export type TAwsConnectionKmsKey = {
alias: string;
id: string;
};
export type TAwsConnectionListKmsKeysResponse = {
kmsKeys: TAwsConnectionKmsKey[];
};
@@ -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 TAwsParameterStoreSync = TRootSecretSync & { export type TAwsParameterStoreSync = TRootSecretSync & {
destination: SecretSync.AWSParameterStore; destination: SecretSync.AWSParameterStore;
@@ -13,4 +13,9 @@ export type TAwsParameterStoreSync = TRootSecretSync & {
name: string; name: string;
id: string; id: string;
}; };
syncOptions: RootSyncOptions & {
keyId?: string;
tags?: { key: string; value?: string }[];
syncSecretMetadataAsTags?: boolean;
};
}; };
@@ -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",
@@ -1,6 +1,12 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSyncInitialSyncBehavior, SecretSyncStatus } from "@app/hooks/api/secretSyncs"; import { SecretSyncInitialSyncBehavior, SecretSyncStatus } from "@app/hooks/api/secretSyncs";
export type RootSyncOptions = {
initialSyncBehavior: SecretSyncInitialSyncBehavior;
// prependPrefix?: string;
// appendSuffix?: string;
};
export type TRootSecretSync = { export type TRootSecretSync = {
id: string; id: string;
name: string; name: string;
@@ -24,11 +30,7 @@ export type TRootSecretSync = {
lastRemoveJobId: string | null; lastRemoveJobId: string | null;
lastRemovedAt: Date | null; lastRemovedAt: Date | null;
lastRemoveMessage: string | null; lastRemoveMessage: string | null;
syncOptions: { syncOptions: RootSyncOptions;
initialSyncBehavior: SecretSyncInitialSyncBehavior;
// prependPrefix?: string;
// appendSuffix?: string;
};
connection: { connection: {
app: AppConnection; app: AppConnection;
id: string; id: string;
@@ -19,7 +19,7 @@ export const SecretSyncsTab = () => {
const { data: secretSyncs = [], isPending: isSecretSyncsPending } = useListSecretSyncs( const { data: secretSyncs = [], isPending: isSecretSyncsPending } = useListSecretSyncs(
currentWorkspace.id, currentWorkspace.id,
{ {
refetchInterval: 4000 refetchInterval: 30000
} }
); );
@@ -37,7 +37,7 @@ const PageContent = () => {
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["editSync"] as const); const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["editSync"] as const);
const { data: secretSync, isPending } = useGetSecretSync(destination, syncId, { const { data: secretSync, isPending } = useGetSecretSync(destination, syncId, {
refetchInterval: 4000 refetchInterval: 30000
}); });
if (isPending) { if (isPending) {
@@ -66,7 +66,7 @@ const PageContent = () => {
const handleEditSource = () => handlePopUpOpen("editSync", SecretSyncEditFields.Source); const handleEditSource = () => handlePopUpOpen("editSync", SecretSyncEditFields.Source);
// const handleEditOptions = () => handlePopUpOpen("editSync", SecretSyncEditFields.Options); const handleEditOptions = () => handlePopUpOpen("editSync", SecretSyncEditFields.Options);
const handleEditDestination = () => handlePopUpOpen("editSync", SecretSyncEditFields.Destination); const handleEditDestination = () => handlePopUpOpen("editSync", SecretSyncEditFields.Destination);
@@ -108,10 +108,7 @@ const PageContent = () => {
<div className="mr-4 flex w-72 flex-col gap-4"> <div className="mr-4 flex w-72 flex-col gap-4">
<SecretSyncDetailsSection secretSync={secretSync} onEditDetails={handleEditDetails} /> <SecretSyncDetailsSection secretSync={secretSync} onEditDetails={handleEditDetails} />
<SecretSyncSourceSection secretSync={secretSync} onEditSource={handleEditSource} /> <SecretSyncSourceSection secretSync={secretSync} onEditSource={handleEditSource} />
<SecretSyncOptionsSection <SecretSyncOptionsSection secretSync={secretSync} onEditOptions={handleEditOptions} />
secretSync={secretSync}
// onEditOptions={handleEditOptions}
/>
</div> </div>
<div className="flex flex-1 flex-col gap-4"> <div className="flex flex-1 flex-col gap-4">
<SecretSyncDestinationSection <SecretSyncDestinationSection
@@ -1,57 +0,0 @@
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP } from "@app/helpers/secretSyncs";
import { TSecretSync } from "@app/hooks/api/secretSyncs";
type Props = {
secretSync: TSecretSync;
// onEditOptions: VoidFunction;
};
export const SecretSyncOptionsSection = ({
secretSync
// onEditOptions
}: Props) => {
const {
destination,
syncOptions: {
// appendSuffix,
// prependPrefix,
initialSyncBehavior
}
} = secretSync;
return (
<div>
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="font-semibold text-mineshaft-100">Sync Options</h3>
{/* <ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.Edit}
a={ProjectPermissionSub.SecretSyncs}
>
{(isAllowed) => (
<IconButton
variant="plain"
colorSchema="secondary"
isDisabled={!isAllowed}
ariaLabel="Edit sync options"
onClick={onEditOptions}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
)}
</ProjectPermissionCan> */}
</div>
<div>
<div className="space-y-3">
<SecretSyncLabel label="Initial Sync Behavior">
{SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destination).name}
</SecretSyncLabel>
{/* <SecretSyncLabel label="Prefix">{prependPrefix}</SecretSyncLabel>
<SecretSyncLabel label="Suffix">{appendSuffix}</SecretSyncLabel> */}
</div>
</div>
</div>
</div>
);
};
@@ -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 { TAwsParameterStoreSync } from "@app/hooks/api/secretSyncs/types/aws-parameter-store-sync";
type Props = {
secretSync: TAwsParameterStoreSync;
};
export const AwsParameterStoreSyncOptionsSection = ({ secretSync }: Props) => {
const {
syncOptions: { keyId, tags, syncSecretMetadataAsTags }
} = secretSync;
return (
<>
{keyId && <SecretSyncLabel label="KMS Key">{keyId}</SecretSyncLabel>}
{tags && tags.length > 0 && (
<SecretSyncLabel label="Resource 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>
)}
</>
);
};
@@ -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>
)}
</>
);
};
@@ -0,0 +1,92 @@
import { ReactNode } from "react";
import { faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { IconButton } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types";
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;
onEditOptions: VoidFunction;
};
export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) => {
const {
destination,
syncOptions: {
// appendSuffix,
// prependPrefix,
initialSyncBehavior
}
} = secretSync;
let AdditionalSyncOptionsComponent: ReactNode;
switch (destination) {
case SecretSync.AWSParameterStore:
AdditionalSyncOptionsComponent = (
<AwsParameterStoreSyncOptionsSection secretSync={secretSync} />
);
break;
case SecretSync.AWSSecretsManager:
AdditionalSyncOptionsComponent = (
<AwsSecretsManagerSyncOptionsSection secretSync={secretSync} />
);
break;
case SecretSync.GitHub:
case SecretSync.GCPSecretManager:
case SecretSync.AzureKeyVault:
case SecretSync.AzureAppConfiguration:
case SecretSync.Databricks:
AdditionalSyncOptionsComponent = null;
break;
default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
}
return (
<div>
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="font-semibold text-mineshaft-100">Sync Options</h3>
{AdditionalSyncOptionsComponent && (
<ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.Edit}
a={ProjectPermissionSub.SecretSyncs}
>
{(isAllowed) => (
<IconButton
variant="plain"
colorSchema="secondary"
isDisabled={!isAllowed}
ariaLabel="Edit sync options"
onClick={onEditOptions}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
)}
</ProjectPermissionCan>
)}
</div>
<div>
<div className="space-y-3">
<SecretSyncLabel label="Initial Sync Behavior">
{SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destination).name}
</SecretSyncLabel>
{/* <SecretSyncLabel label="Prefix">{prependPrefix}</SecretSyncLabel>
<SecretSyncLabel label="Suffix">{appendSuffix}</SecretSyncLabel> */}
{AdditionalSyncOptionsComponent}
</div>
</div>
</div>
</div>
);
};
@@ -0,0 +1 @@
export * from "./SecretSyncOptionsSection";