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.`
};
},
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: {
AWS_PARAMETER_STORE: {
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 {
CreateAwsConnectionSchema,
SanitizedAwsConnectionSchema,
UpdateAwsConnectionSchema
} 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";
export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
export const registerAwsConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.AWS,
server,
@@ -15,3 +21,42 @@ export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
createSchema: CreateAwsConnectionSchema,
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,
TValidateAppConnectionCredentials
} 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 { TAppConnectionDALFactory } from "./app-connection-dal";
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
import { awsConnectionService } from "./aws/aws-connection-service";
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
import { databricksConnectionService } from "./databricks/databricks-connection-service";
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
import { gcpConnectionService } from "./gcp/gcp-connection-service";
import { ValidateGitHubConnectionCredentialsSchema } from "./github";
import { githubConnectionService } from "./github/github-connection-service";
export type TAppConnectionServiceFactoryDep = {
appConnectionDAL: TAppConnectionDALFactory;
@@ -369,6 +370,7 @@ export const appConnectionServiceFactory = ({
listAvailableAppConnectionsForUser,
github: githubConnectionService(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 {
TAwsConnection,
TAwsConnectionConfig,
@@ -16,6 +17,7 @@ import {
TGitHubConnectionInput,
TValidateGitHubConnectionCredentials
} from "@app/services/app-connection/github";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
TAzureAppConfigurationConnection,
@@ -73,3 +75,9 @@ export type TValidateAppConnectionCredentials =
| TValidateAzureKeyVaultConnectionCredentials
| TValidateAzureAppConfigurationConnectionCredentials
| 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 { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { Knex } from "knex";
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";
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 BATCH_SIZE = 10;
@@ -80,6 +82,122 @@ const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSPara
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 (
ssm: AWS.SSM,
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 (
ssm: AWS.SSM,
parameters: AWS.SSM.Parameter[],
@@ -132,34 +286,75 @@ const deleteParametersBatch = async (
export const AwsParameterStoreSyncFns = {
syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => {
const { destinationConfig } = secretSync;
const { destinationConfig, syncOptions } = secretSync;
const ssm = await getSSM(secretSync);
// TODO(scott): KMS Key ID, Tags
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
for await (const entry of Object.entries(secretMap)) {
const [key, { value }] = entry;
const awsParameterStoreMetadataRecord = await getParameterMetadataByPath(ssm, destinationConfig.path);
// skip empty values (not allowed by AWS) or secrets that haven't changed
if (!value || (key in awsParameterStoreSecretsRecord && awsParameterStoreSecretsRecord[key].Value === value)) {
const awsParameterStoreTagsRecord = await getParameterStoreTagsRecord(
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
continue;
}
try {
await putParameter(ssm, {
Name: `${destinationConfig.path}${key}`,
Type: "SecureString",
Value: value,
Overwrite: true
// create parameter or update if changed
if (
!(key in awsParameterStoreSecretsRecord) ||
value !== awsParameterStoreSecretsRecord[key].Value ||
syncOptions.keyId !== awsParameterStoreMetadataRecord[key]?.KeyId
) {
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,
secretKey: key
}
if (tagKeysToRemove.length) {
await removeTagsFromParameter(ssm, {
ResourceId: `${destinationConfig.path}${key}`,
TagKeys: tagKeysToRemove
});
}
}
@@ -8,6 +8,7 @@ import {
GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
const AwsParameterStoreSyncDestinationConfigSchema = z.object({
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)
});
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),
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
});
export const CreateAwsParameterStoreSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.AWSParameterStore
SecretSync.AWSParameterStore,
AwsParameterStoreSyncOptionsConfig,
AwsParameterStoreSyncOptionsSchema
).extend({
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
});
export const UpdateAwsParameterStoreSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.AWSParameterStore
SecretSync.AWSParameterStore,
AwsParameterStoreSyncOptionsConfig,
AwsParameterStoreSyncOptionsSchema
).extend({
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema.optional()
});
@@ -9,6 +9,7 @@ import {
GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
const AwsSecretsManagerSyncDestinationConfigSchema = z
.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),
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
});
export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.AWSSecretsManager
SecretSync.AWSSecretsManager,
AwsSecretsManagerSyncOptionsConfig
).extend({
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
});
export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.AWSSecretsManager
SecretSync.AWSSecretsManager,
AwsSecretsManagerSyncOptionsConfig
).extend({
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional()
});
@@ -233,6 +233,7 @@ export const secretSyncQueueFactory = ({
}
secretMap[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
secretMap[secretKey].secretMetadata = secret.secretMetadata;
})
);
@@ -258,7 +259,8 @@ export const secretSyncQueueFactory = ({
secretMap[importedSecret.key] = {
skipMultilineEncoding: importedSecret.skipMultilineEncoding,
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 { 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 { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
const SyncOptionsSchema = (secretSync: SecretSync, options: TSyncOptionsConfig = { canImportSecrets: true }) =>
z.object({
initialSyncBehavior: (options.canImportSecrets
const BaseSyncOptionsSchema = <T extends AnyZodObject | undefined = undefined>({
destination,
syncOptionsConfig: { canImportSecrets },
merge,
isUpdateSchema
}: {
destination: SecretSync;
syncOptionsConfig: TSyncOptionsConfig;
merge?: T;
isUpdateSchema?: boolean;
}) => {
const baseSchema = z.object({
initialSyncBehavior: (canImportSecrets
? z.nativeEnum(SecretSyncInitialSyncBehavior)
: z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination)
).describe(SecretSyncs.SYNC_OPTIONS(secretSync).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)
).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior)
});
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({
destination: true,
destinationConfig: true,
syncOptions: true
}).extend({
// destination needs to be on the extended object for type differentiation
syncOptions: SyncOptionsSchema(destination, syncOptionsConfig),
syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge }),
// join properties
projectId: z.string(),
connection: z.object({
@@ -47,7 +58,11 @@ export const BaseSecretSyncSchema = (destination: SecretSync, syncOptionsConfig?
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({
name: slugSchema({ field: "name" }).describe(SecretSyncs.CREATE(destination).name),
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)
.describe(SecretSyncs.CREATE(destination).secretPath),
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({
name: slugSchema({ field: "name" }).describe(SecretSyncs.UPDATE(destination).name).optional(),
connectionId: z.string().uuid().describe(SecretSyncs.UPDATE(destination).connectionId).optional(),
@@ -90,7 +109,5 @@ export const GenericUpdateSecretSyncFieldsSchema = (destination: SecretSync, syn
.optional()
.describe(SecretSyncs.UPDATE(destination).secretPath),
isAutoSyncEnabled: z.boolean().optional().describe(SecretSyncs.UPDATE(destination).isAutoSyncEnabled),
syncOptions: SyncOptionsSchema(destination, syncOptionsConfig)
.optional()
.describe(SecretSyncs.UPDATE(destination).syncOptions)
syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge, isUpdateSchema: true })
});
@@ -2,6 +2,7 @@ import { Job } from "bullmq";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { QueueJobs } from "@app/queue";
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
import {
TAwsSecretsManagerSync,
TAwsSecretsManagerSyncInput,
@@ -197,5 +198,10 @@ export type TSendSecretSyncFailedNotificationsJobDTO = Job<
export type TSecretMap = Record<
string,
{ value: string; comment?: string; skipMultilineEncoding?: boolean | null | undefined }
{
value: string;
comment?: string;
skipMultilineEncoding?: boolean | null | undefined;
secretMetadata?: ResourceMetadataDTO;
}
>;