feature: add support for kms key, tags, and syncing secret metadata to aws parameter store sync

This commit is contained in:
Scott Wilson
2025-02-18 20:28:27 -08:00
parent 21c6700db2
commit b0cd5bd10d
42 changed files with 1181 additions and 271 deletions
+7
View File
@@ -1722,6 +1722,13 @@ 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.`
}
},
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,67 @@
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 { Aliases = [] } = await awsKms.listAliases({ Limit: 100 }).promise();
const aliasEntries = Aliases.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;
return true;
});
const kmsKeys = aliasEntries.map((alias) => {
return {
id: alias.TargetKeyId!,
alias: alias.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
};
};
@@ -1,8 +1,9 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify } from "@app/lib/knex";
import { Knex } from "knex";
export type TKmsRootConfigDALFactory = ReturnType<typeof kmsRootConfigDALFactory>; export type TKmsRootConfigDALFactory = ReturnType<typeof kmsRootConfigDALFactory>;
@@ -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,122 @@ 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,
path: string,
awsParameterStoreSecretsRecord: TAWSParameterStoreRecord,
areTagsPresent: boolean
): Promise<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" && !areTagsPresent) {
return {};
}
throw e;
}
}
return 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 +216,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,34 +286,75 @@ 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 awsParameterStoreTagsRecord = await getParameterStoreTagsRecord(
if (!value || (key in awsParameterStoreSecretsRecord && awsParameterStoreSecretsRecord[key].Value === value)) { ssm,
destinationConfig.path,
awsParameterStoreSecretsRecord,
Boolean(syncOptions.tags || 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 { // create parameter or update if changed
await putParameter(ssm, { if (
Name: `${destinationConfig.path}${key}`, !(key in awsParameterStoreSecretsRecord) ||
Type: "SecureString", value !== awsParameterStoreSecretsRecord[key].Value ||
Value: value, syncOptions.keyId !== awsParameterStoreMetadataRecord[key]?.KeyId
Overwrite: true ) {
try {
await putParameter(ssm, {
Name: `${destinationConfig.path}${key}`,
Type: "SecureString",
Value: value,
Overwrite: true,
KeyId: syncOptions.keyId
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
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) {
await addTagsToParameter(ssm, {
ResourceId: `${destinationConfig.path}${key}`,
Tags: tagsToAdd
}); });
} catch (error) { }
throw new SecretSyncError({
error, if (tagKeysToRemove.length) {
secretKey: key await removeTagsFromParameter(ssm, {
ResourceId: `${destinationConfig.path}${key}`,
TagKeys: tagKeysToRemove
}); });
} }
} }
@@ -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,65 @@ 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,
"Tag keys can only contain Unicode letters, digits, white space and any of the following: _."
)
.min(1, "AWS tag key required")
.max(128, "AWS tag name cannot exceed 128 characters"),
value: z
.string()
.regex(/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u, "Invalid AWS tag value")
.max(256, "Tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-")
})
.array()
.max(50)
.refine((items) => new Set(items.map((item) => item.key)).size === items.length, {
message: "AWS 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()
}); });
@@ -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,19 +39,26 @@ const AwsSecretsManagerSyncDestinationConfigSchema = z
}) })
); );
export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(SecretSync.AWSSecretsManager).extend({ const AwsSecretsManagerSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(
SecretSync.AWSSecretsManager,
AwsSecretsManagerSyncOptionsConfig
).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,
AwsSecretsManagerSyncOptionsConfig
).extend({ ).extend({
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
}); });
export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema( export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.AWSSecretsManager SecretSync.AWSSecretsManager,
AwsSecretsManagerSyncOptionsConfig
).extend({ ).extend({
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional() destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional()
}); });
@@ -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.

Before

Width:  |  Height:  |  Size: 659 KiB

After

Width:  |  Height:  |  Size: 885 KiB

@@ -118,7 +118,9 @@ Infisical supports two methods for connecting to AWS.
"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
"ssm:RemoveTagsFromResource", // if you need to add tags to secrets
"kms:ListKeys", // if you need to specify the KMS key "kms:ListKeys", // if you need to specify the KMS key
"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
@@ -259,7 +261,9 @@ Infisical supports two methods for connecting to AWS.
"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
"ssm:RemoveTagsFromResource", // if you need to add tags to secrets
"kms:ListKeys", // if you need to specify the KMS key "kms:ListKeys", // if you need to specify the KMS key
"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
@@ -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.
- **Key ID**: 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**.
@@ -16,10 +16,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 +32,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: [] }
]; ];
@@ -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">
@@ -0,0 +1,184 @@
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)
}
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-[40vh] 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>
)}
/>
</>
);
};
@@ -1,12 +1,14 @@
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";
type Props = { type Props = {
hideInitialSync?: boolean; hideInitialSync?: boolean;
@@ -21,6 +23,24 @@ 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:
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 +111,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?.length && (
<SecretSyncLabel label="AWS 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="AWS 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);
@@ -3,15 +3,18 @@ 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 { 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 +22,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,7 +42,8 @@ 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 />;
@@ -84,7 +89,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 +102,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, "AWS tag name 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,33 @@
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().merge(
destination: z.literal(SecretSync.AWSSecretsManager), z.object({
destinationConfig: z destination: z.literal(SecretSync.AWSSecretsManager),
.discriminatedUnion("mappingBehavior", [ destinationConfig: z
z.object({ .discriminatedUnion("mappingBehavior", [
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.OneToOne) z.object({
}), mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.OneToOne)
z.object({ }),
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne), z.object({
secretName: z mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne),
.string() secretName: z
.regex( .string()
/^[a-zA-Z0-9/_+=.@-]+$/, .regex(
"Secret name must contain only alphanumeric characters and the characters /_+=.@-" /^[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") .min(1, "Secret name is required")
}) .max(256, "Secret name cannot exceed 256 characters")
]) })
.and( ])
z.object({ .and(
region: z.string().min(1, "Region required") 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,24 @@
import { SecretSync } from "@app/hooks/api/secretSyncs";
export type TDatabricksSecretScope = {
name: string;
};
export type TDatabricksConnectionListSecretScopesResponse = {
secretScopes: TDatabricksSecretScope[];
};
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,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;
@@ -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?.length && (
<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,87 @@
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";
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:
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";