with stripSchema and filterForSchema

This commit is contained in:
x032205
2025-05-13 23:08:54 -04:00
parent f6e1441dc0
commit bae62421ae
14 changed files with 275 additions and 151 deletions
+1
View File
@@ -2109,6 +2109,7 @@ export const SecretSyncs = {
const destinationName = SECRET_SYNC_NAME_MAP[destination]; const destinationName = SECRET_SYNC_NAME_MAP[destination];
return { return {
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.`,
keySchema: `Specify the format to use for structuring secret keys in the ${destinationName} destination.`,
disableSecretDeletion: `Enable this flag to prevent removal of secrets from the ${destinationName} destination when syncing.` disableSecretDeletion: `Enable this flag to prevent removal of secrets from the ${destinationName} destination when syncing.`
}; };
}, },
@@ -367,7 +367,17 @@ export const registerSyncSecretsEndpoints = <T extends TSecretSync, I extends TS
querystring: z.object({ querystring: z.object({
importBehavior: z importBehavior: z
.nativeEnum(SecretSyncImportBehavior) .nativeEnum(SecretSyncImportBehavior)
.describe(SecretSyncs.IMPORT_SECRETS(destination).importBehavior) .describe(SecretSyncs.IMPORT_SECRETS(destination).importBehavior),
filterForSchema: z
.enum(["true", "false"])
.optional()
.default("false")
.transform((v) => v === "true"),
stripSchema: z
.enum(["true", "false"])
.optional()
.default("false")
.transform((v) => v === "true")
}), }),
response: { response: {
200: z.object({ secretSync: responseSchema }) 200: z.object({ secretSync: responseSchema })
@@ -376,13 +386,15 @@ export const registerSyncSecretsEndpoints = <T extends TSecretSync, I extends TS
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { syncId } = req.params; const { syncId } = req.params;
const { importBehavior } = req.query; const { importBehavior, filterForSchema, stripSchema } = req.query;
const secretSync = (await server.services.secretSync.triggerSecretSyncImportSecretsById( const secretSync = (await server.services.secretSync.triggerSecretSyncImportSecretsById(
{ {
syncId, syncId,
destination, destination,
importBehavior importBehavior,
filterForSchema,
stripSchema
}, },
req.permission req.permission
)) as T; )) as T;
@@ -61,45 +61,63 @@ type TSyncSecretDeps = {
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}; };
// const addAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => { interface TSyncSecretConfig {
// let secretMap = { ...unprocessedSecretMap }; filterForSchema?: boolean;
// stripSchema?: boolean;
// const { appendSuffix, prependPrefix } = secretSync.syncOptions; }
//
// if (appendSuffix || prependPrefix) { // Add schema to secret keys
// secretMap = {}; const addSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecretMap => {
// Object.entries(unprocessedSecretMap).forEach(([key, value]) => { if (!schema) return unprocessedSecretMap;
// secretMap[`${prependPrefix || ""}${key}${appendSuffix || ""}`] = value;
// }); const processedSecretMap: TSecretMap = {};
// }
// for (const [key, value] of Object.entries(unprocessedSecretMap)) {
// return secretMap; const newKey = schema.replace("{{secretKey}}", key);
// }; processedSecretMap[newKey] = value;
// }
// const stripAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => {
// let secretMap = { ...unprocessedSecretMap }; return processedSecretMap;
// };
// const { appendSuffix, prependPrefix } = secretSync.syncOptions;
// // Strip schema from secret keys
// if (appendSuffix || prependPrefix) { const stripSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecretMap => {
// secretMap = {}; if (!schema) return unprocessedSecretMap;
// Object.entries(unprocessedSecretMap).forEach(([key, value]) => {
// let processedKey = key; const [prefix, suffix] = schema.split("{{secretKey}}");
//
// if (prependPrefix && processedKey.startsWith(prependPrefix)) { const strippedMap: TSecretMap = {};
// processedKey = processedKey.slice(prependPrefix.length);
// } for (const [key, value] of Object.entries(unprocessedSecretMap)) {
// if (!key.startsWith(prefix) || !key.endsWith(suffix)) {
// if (appendSuffix && processedKey.endsWith(appendSuffix)) { // eslint-disable-next-line no-continue
// processedKey = processedKey.slice(0, -appendSuffix.length); continue;
// } }
//
// secretMap[processedKey] = value; const strippedKey = key.slice(prefix.length, key.length - suffix.length);
// }); strippedMap[strippedKey] = value;
// } }
//
// return secretMap; return strippedMap;
// }; };
// Filter only for secrets with keys that match the schema
const filterForSchema = (secretMap: TSecretMap, schema?: string): TSecretMap => {
if (!schema) return secretMap;
const [prefix, suffix] = schema.split("{{secretKey}}");
if (prefix === undefined || suffix === undefined) return secretMap;
const filteredMap: TSecretMap = {};
for (const [key, value] of Object.entries(secretMap)) {
if (key.startsWith(prefix) && key.endsWith(suffix)) {
filteredMap[key] = value;
}
}
return filteredMap;
};
export const SecretSyncFns = { export const SecretSyncFns = {
syncSecrets: ( syncSecrets: (
@@ -107,51 +125,51 @@ export const SecretSyncFns = {
secretMap: TSecretMap, secretMap: TSecretMap,
{ kmsService, appConnectionDAL }: TSyncSecretDeps { kmsService, appConnectionDAL }: TSyncSecretDeps
): Promise<void> => { ): Promise<void> => {
// const affixedSecretMap = addAffixes(secretSync, secretMap); const schemaSecretMap = addSchema(secretMap, secretSync.syncOptions.keySchema);
switch (secretSync.destination) { switch (secretSync.destination) {
case SecretSync.AWSParameterStore: case SecretSync.AWSParameterStore:
return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap); return AwsParameterStoreSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.AWSSecretsManager: case SecretSync.AWSSecretsManager:
return AwsSecretsManagerSyncFns.syncSecrets(secretSync, secretMap); return AwsSecretsManagerSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.GitHub: case SecretSync.GitHub:
return GithubSyncFns.syncSecrets(secretSync, secretMap); return GithubSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.GCPSecretManager: case SecretSync.GCPSecretManager:
return GcpSyncFns.syncSecrets(secretSync, secretMap); return GcpSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.AzureKeyVault: case SecretSync.AzureKeyVault:
return azureKeyVaultSyncFactory({ return azureKeyVaultSyncFactory({
appConnectionDAL, appConnectionDAL,
kmsService kmsService
}).syncSecrets(secretSync, secretMap); }).syncSecrets(secretSync, schemaSecretMap);
case SecretSync.AzureAppConfiguration: case SecretSync.AzureAppConfiguration:
return azureAppConfigurationSyncFactory({ return azureAppConfigurationSyncFactory({
appConnectionDAL, appConnectionDAL,
kmsService kmsService
}).syncSecrets(secretSync, secretMap); }).syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Databricks: case SecretSync.Databricks:
return databricksSyncFactory({ return databricksSyncFactory({
appConnectionDAL, appConnectionDAL,
kmsService kmsService
}).syncSecrets(secretSync, secretMap); }).syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Humanitec: case SecretSync.Humanitec:
return HumanitecSyncFns.syncSecrets(secretSync, secretMap); return HumanitecSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.TerraformCloud: case SecretSync.TerraformCloud:
return TerraformCloudSyncFns.syncSecrets(secretSync, secretMap); return TerraformCloudSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Camunda: case SecretSync.Camunda:
return camundaSyncFactory({ return camundaSyncFactory({
appConnectionDAL, appConnectionDAL,
kmsService kmsService
}).syncSecrets(secretSync, secretMap); }).syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Vercel: case SecretSync.Vercel:
return VercelSyncFns.syncSecrets(secretSync, secretMap); return VercelSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Windmill: case SecretSync.Windmill:
return WindmillSyncFns.syncSecrets(secretSync, secretMap); return WindmillSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.HCVault: case SecretSync.HCVault:
return HCVaultSyncFns.syncSecrets(secretSync, secretMap); return HCVaultSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.TeamCity: case SecretSync.TeamCity:
return TeamCitySyncFns.syncSecrets(secretSync, secretMap); return TeamCitySyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.OCIVault: case SecretSync.OCIVault:
return OCIVaultSyncFns.syncSecrets(secretSync, secretMap); return OCIVaultSyncFns.syncSecrets(secretSync, schemaSecretMap);
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -160,7 +178,8 @@ export const SecretSyncFns = {
}, },
getSecrets: async ( getSecrets: async (
secretSync: TSecretSyncWithCredentials, secretSync: TSecretSyncWithCredentials,
{ kmsService, appConnectionDAL }: TSyncSecretDeps { kmsService, appConnectionDAL }: TSyncSecretDeps,
config?: TSyncSecretConfig
): Promise<TSecretMap> => { ): Promise<TSecretMap> => {
let secretMap: TSecretMap; let secretMap: TSecretMap;
switch (secretSync.destination) { switch (secretSync.destination) {
@@ -226,59 +245,68 @@ export const SecretSyncFns = {
); );
} }
return secretMap; let processedSecretMap = secretMap;
// return stripAffixes(secretSync, secretMap);
if (config?.filterForSchema) {
processedSecretMap = filterForSchema(processedSecretMap);
}
if (config?.stripSchema) {
return stripSchema(processedSecretMap, secretSync.syncOptions.keySchema);
}
return processedSecretMap;
}, },
removeSecrets: ( removeSecrets: (
secretSync: TSecretSyncWithCredentials, secretSync: TSecretSyncWithCredentials,
secretMap: TSecretMap, secretMap: TSecretMap,
{ kmsService, appConnectionDAL }: TSyncSecretDeps { kmsService, appConnectionDAL }: TSyncSecretDeps
): Promise<void> => { ): Promise<void> => {
// const affixedSecretMap = addAffixes(secretSync, secretMap); const schemaSecretMap = addSchema(secretMap, secretSync.syncOptions.keySchema);
switch (secretSync.destination) { switch (secretSync.destination) {
case SecretSync.AWSParameterStore: case SecretSync.AWSParameterStore:
return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap); return AwsParameterStoreSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.AWSSecretsManager: case SecretSync.AWSSecretsManager:
return AwsSecretsManagerSyncFns.removeSecrets(secretSync, secretMap); return AwsSecretsManagerSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.GitHub: case SecretSync.GitHub:
return GithubSyncFns.removeSecrets(secretSync, secretMap); return GithubSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.GCPSecretManager: case SecretSync.GCPSecretManager:
return GcpSyncFns.removeSecrets(secretSync, secretMap); return GcpSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.AzureKeyVault: case SecretSync.AzureKeyVault:
return azureKeyVaultSyncFactory({ return azureKeyVaultSyncFactory({
appConnectionDAL, appConnectionDAL,
kmsService kmsService
}).removeSecrets(secretSync, secretMap); }).removeSecrets(secretSync, schemaSecretMap);
case SecretSync.AzureAppConfiguration: case SecretSync.AzureAppConfiguration:
return azureAppConfigurationSyncFactory({ return azureAppConfigurationSyncFactory({
appConnectionDAL, appConnectionDAL,
kmsService kmsService
}).removeSecrets(secretSync, secretMap); }).removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Databricks: case SecretSync.Databricks:
return databricksSyncFactory({ return databricksSyncFactory({
appConnectionDAL, appConnectionDAL,
kmsService kmsService
}).removeSecrets(secretSync, secretMap); }).removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Humanitec: case SecretSync.Humanitec:
return HumanitecSyncFns.removeSecrets(secretSync, secretMap); return HumanitecSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.TerraformCloud: case SecretSync.TerraformCloud:
return TerraformCloudSyncFns.removeSecrets(secretSync, secretMap); return TerraformCloudSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Camunda: case SecretSync.Camunda:
return camundaSyncFactory({ return camundaSyncFactory({
appConnectionDAL, appConnectionDAL,
kmsService kmsService
}).removeSecrets(secretSync, secretMap); }).removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Vercel: case SecretSync.Vercel:
return VercelSyncFns.removeSecrets(secretSync, secretMap); return VercelSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Windmill: case SecretSync.Windmill:
return WindmillSyncFns.removeSecrets(secretSync, secretMap); return WindmillSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.HCVault: case SecretSync.HCVault:
return HCVaultSyncFns.removeSecrets(secretSync, secretMap); return HCVaultSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.TeamCity: case SecretSync.TeamCity:
return TeamCitySyncFns.removeSecrets(secretSync, secretMap); return TeamCitySyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.OCIVault: case SecretSync.OCIVault:
return OCIVaultSyncFns.removeSecrets(secretSync, secretMap); return OCIVaultSyncFns.removeSecrets(secretSync, schemaSecretMap);
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -319,9 +319,12 @@ export const secretSyncQueueFactory = ({
); );
}; };
// TODO(andrey): Possibly add a "stripSchema" parameter for imports?
const $importSecrets = async ( const $importSecrets = async (
secretSync: TSecretSyncWithCredentials, secretSync: TSecretSyncWithCredentials,
importBehavior: SecretSyncImportBehavior importBehavior: SecretSyncImportBehavior,
filterForSchema: boolean,
stripSchema: boolean
): Promise<TSecretMap> => { ): Promise<TSecretMap> => {
const { projectId, environment, folder } = secretSync; const { projectId, environment, folder } = secretSync;
@@ -330,10 +333,17 @@ export const secretSyncQueueFactory = ({
"Invalid Secret Sync source configuration: folder no longer exists. Please update source environment and secret path." "Invalid Secret Sync source configuration: folder no longer exists. Please update source environment and secret path."
); );
const importedSecrets = await SecretSyncFns.getSecrets(secretSync, { const importedSecrets = await SecretSyncFns.getSecrets(
appConnectionDAL, secretSync,
kmsService {
}); appConnectionDAL,
kmsService
},
{
filterForSchema,
stripSchema
}
);
if (!Object.keys(importedSecrets).length) return {}; if (!Object.keys(importedSecrets).length) return {};
@@ -439,11 +449,14 @@ export const secretSyncQueueFactory = ({
const secretMap = await $getInfisicalSecrets(secretSync); const secretMap = await $getInfisicalSecrets(secretSync);
if (!lastSyncedAt && initialSyncBehavior !== SecretSyncInitialSyncBehavior.OverwriteDestination) { if (!lastSyncedAt && initialSyncBehavior !== SecretSyncInitialSyncBehavior.OverwriteDestination) {
// TODO(andrey): Possibly add a way to filter / strip schemas on initial sync?
const importedSecretMap = await $importSecrets( const importedSecretMap = await $importSecrets(
secretSyncWithCredentials, secretSyncWithCredentials,
initialSyncBehavior === SecretSyncInitialSyncBehavior.ImportPrioritizeSource initialSyncBehavior === SecretSyncInitialSyncBehavior.ImportPrioritizeSource
? SecretSyncImportBehavior.PrioritizeSource ? SecretSyncImportBehavior.PrioritizeSource
: SecretSyncImportBehavior.PrioritizeDestination : SecretSyncImportBehavior.PrioritizeDestination,
false,
false
); );
Object.entries(importedSecretMap).forEach(([key, secretData]) => { Object.entries(importedSecretMap).forEach(([key, secretData]) => {
@@ -535,7 +548,7 @@ export const secretSyncQueueFactory = ({
const $handleImportSecretsJob = async (job: TSecretSyncImportSecretsDTO) => { const $handleImportSecretsJob = async (job: TSecretSyncImportSecretsDTO) => {
const { const {
data: { syncId, auditLogInfo, importBehavior } data: { syncId, auditLogInfo, importBehavior, filterForSchema, stripSchema }
} = job; } = job;
const secretSync = await secretSyncDAL.findById(syncId); const secretSync = await secretSyncDAL.findById(syncId);
@@ -573,7 +586,9 @@ export const secretSyncQueueFactory = ({
credentials credentials
} }
} as TSecretSyncWithCredentials, } as TSecretSyncWithCredentials,
importBehavior importBehavior,
filterForSchema,
stripSchema
); );
isSuccess = true; isSuccess = true;
@@ -1,3 +1,4 @@
import RE2 from "re2";
import { AnyZodObject, z } from "zod"; import { AnyZodObject, z } from "zod";
import { SecretSyncsSchema } from "@app/db/schemas/secret-syncs"; import { SecretSyncsSchema } from "@app/db/schemas/secret-syncs";
@@ -24,6 +25,14 @@ const BaseSyncOptionsSchema = <T extends AnyZodObject | undefined = undefined>({
? z.nativeEnum(SecretSyncInitialSyncBehavior) ? z.nativeEnum(SecretSyncInitialSyncBehavior)
: z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination) : z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination)
).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior), ).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior),
keySchema: z
.string()
.optional()
.refine((val) => !val || new RE2(/^(?:[a-zA-Z0-9\-/]*)(?:\{\{secretKey\}\})(?:[a-zA-Z0-9\-/]*)$/).test(val), {
message:
"Key schema must include {{secretKey}} and only contain letters, numbers, dashes, slashes, and the {{secretKey}} placeholder."
})
.describe(SecretSyncs.SYNC_OPTIONS(destination).keySchema),
disableSecretDeletion: z.boolean().optional().describe(SecretSyncs.SYNC_OPTIONS(destination).disableSecretDeletion) disableSecretDeletion: z.boolean().optional().describe(SecretSyncs.SYNC_OPTIONS(destination).disableSecretDeletion)
}); });
@@ -231,6 +231,8 @@ export type TQueueSecretSyncImportSecretsByIdDTO = {
syncId: string; syncId: string;
importBehavior: SecretSyncImportBehavior; importBehavior: SecretSyncImportBehavior;
auditLogInfo?: AuditLogInfo; auditLogInfo?: AuditLogInfo;
filterForSchema: boolean;
stripSchema: boolean;
}; };
export type TTriggerSecretSyncImportSecretsByIdDTO = { export type TTriggerSecretSyncImportSecretsByIdDTO = {
@@ -10,7 +10,8 @@ import {
ModalClose, ModalClose,
ModalContent, ModalContent,
Select, Select,
SelectItem SelectItem,
Switch
} from "@app/components/v2"; } from "@app/components/v2";
import { SECRET_SYNC_IMPORT_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { SECRET_SYNC_IMPORT_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { import {
@@ -31,30 +32,49 @@ type ContentProps = {
}; };
const FormSchema = z.object({ const FormSchema = z.object({
importBehavior: z.nativeEnum(SecretSyncImportBehavior) importBehavior: z.nativeEnum(SecretSyncImportBehavior),
filterForSchema: z.boolean(),
stripSchema: z.boolean()
}); });
type TFormData = z.infer<typeof FormSchema>; type TFormData = z.infer<typeof FormSchema>;
const Content = ({ secretSync, onComplete }: ContentProps) => { const Content = ({ secretSync, onComplete }: ContentProps) => {
const { id: syncId, destination, projectId } = secretSync; const {
id: syncId,
destination,
projectId,
syncOptions: { keySchema }
} = secretSync;
const destinationName = SECRET_SYNC_MAP[destination].name; const destinationName = SECRET_SYNC_MAP[destination].name;
const { const {
handleSubmit, handleSubmit,
control, control,
formState: { isSubmitting, isDirty } formState: { isSubmitting, isDirty }
} = useForm<TFormData>({ resolver: zodResolver(FormSchema) }); } = useForm<TFormData>({
resolver: zodResolver(FormSchema),
defaultValues: {
filterForSchema: false,
stripSchema: false
}
});
const triggerImportSecrets = useTriggerSecretSyncImportSecrets(); const triggerImportSecrets = useTriggerSecretSyncImportSecrets();
const handleTriggerImportSecrets = async ({ importBehavior }: TFormData) => { const handleTriggerImportSecrets = async ({
importBehavior,
filterForSchema,
stripSchema
}: TFormData) => {
try { try {
await triggerImportSecrets.mutateAsync({ await triggerImportSecrets.mutateAsync({
syncId, syncId,
destination, destination,
importBehavior, importBehavior,
projectId projectId,
filterForSchema,
stripSchema
}); });
createNotification({ createNotification({
@@ -131,6 +151,64 @@ const Content = ({ secretSync, onComplete }: ContentProps) => {
</FormControl> </FormControl>
)} )}
/> />
{keySchema && (
<>
<Controller
control={control}
name="filterForSchema"
defaultValue
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipClassName="max-w-md"
tooltipText={
<div className="flex flex-col gap-2">
<p>
If enabled, Infisical will only import destination secrets that match your key
schema:
</p>
<code className="text-mineshaft-300">{keySchema}</code>
</div>
}
label="Filter Keys for Schema"
isError={Boolean(error)}
errorText={error?.message}
>
<Switch
id="filter-for-schema"
thumbClassName="bg-mineshaft-800"
isChecked={value}
onCheckedChange={onChange}
>
Only import destination secrets that match schema
</Switch>
</FormControl>
)}
/>
<Controller
control={control}
name="stripSchema"
defaultValue
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipClassName="max-w-md"
tooltipText="If enabled, Infisical will strip secret keys according to your schema. Keys that do not match the schema will not be affected."
label="Strip Schema"
isError={Boolean(error)}
errorText={error?.message}
>
<Switch
id="strip-schema"
thumbClassName="bg-mineshaft-800"
isChecked={value}
onCheckedChange={onChange}
>
Strip schema from imported secret keys
</Switch>
</FormControl>
)}
/>
</>
)}
<div className="mt-8 flex w-full items-center justify-between gap-2"> <div className="mt-8 flex w-full items-center justify-between gap-2">
<ModalClose asChild> <ModalClose asChild>
<Button colorSchema="secondary" variant="plain"> <Button colorSchema="secondary" variant="plain">
@@ -3,7 +3,7 @@ import { Controller, useFormContext } from "react-hook-form";
import { faQuestionCircle, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons"; import { faQuestionCircle, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FormControl, Select, SelectItem, Switch, Tooltip } from "@app/components/v2"; import { FormControl, Input, Select, SelectItem, Switch, Tooltip } 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, useSecretSyncOption } from "@app/hooks/api/secretSyncs"; import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs";
@@ -122,6 +122,22 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
)} )}
</> </>
)} )}
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipClassName="max-w-md"
tooltipText="When a secret is synced, it's key will be injected into the key schema before it reaches the destination. This is useful for organization."
isError={Boolean(error)}
isOptional
errorText={error?.message}
label="Key Schema"
>
<Input value={value} onChange={onChange} placeholder="prefix/{{secretKey}}/suffix" />
</FormControl>
)}
control={control}
name="syncOptions.keySchema"
/>
{AdditionalSyncOptionsFieldsComponent} {AdditionalSyncOptionsFieldsComponent}
<Controller <Controller
control={control} control={control}
@@ -161,34 +177,6 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
); );
}} }}
/> />
{/* <Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
isOptional
errorText={error?.message}
label="Prepend Prefix"
>
<Input className="uppercase" value={value} onChange={onChange} placeholder="INF_" />
</FormControl>
)}
control={control}
name="syncOptions.prependPrefix"
/>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
isOptional
errorText={error?.message}
label="Append Suffix"
>
<Input className="uppercase" value={value} onChange={onChange} placeholder="_INF" />
</FormControl>
)}
control={control}
name="syncOptions.appendSuffix"
/> */}
</> </>
); );
}; };
@@ -41,11 +41,7 @@ export const SecretSyncReviewFields = () => {
connection, connection,
environment, environment,
secretPath, secretPath,
syncOptions: { syncOptions: { disableSecretDeletion, initialSyncBehavior, keySchema },
// appendSuffix, prependPrefix,
disableSecretDeletion,
initialSyncBehavior
},
destination, destination,
isAutoSyncEnabled isAutoSyncEnabled
} = watch(); } = watch();
@@ -137,8 +133,7 @@ export const SecretSyncReviewFields = () => {
<GenericFieldLabel label="Initial Sync Behavior"> <GenericFieldLabel label="Initial Sync Behavior">
{SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destinationName).name} {SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destinationName).name}
</GenericFieldLabel> </GenericFieldLabel>
{/* <SecretSyncLabel label="Prepend Prefix">{prependPrefix}</SecretSyncLabel> <GenericFieldLabel label="Key Schema">{keySchema}</GenericFieldLabel>
<SecretSyncLabel label="Append Suffix">{appendSuffix}</SecretSyncLabel> */}
{AdditionalSyncOptionsFieldsComponent} {AdditionalSyncOptionsFieldsComponent}
{disableSecretDeletion && ( {disableSecretDeletion && (
<GenericFieldLabel label="Secret Deletion"> <GenericFieldLabel label="Secret Deletion">
@@ -8,18 +8,17 @@ export const BaseSecretSyncSchema = <T extends AnyZodObject | undefined = undefi
) => { ) => {
const baseSyncOptionsSchema = z.object({ const baseSyncOptionsSchema = z.object({
initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior), initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior),
disableSecretDeletion: z.boolean().optional().default(false) disableSecretDeletion: z.boolean().optional().default(false),
// scott: removed temporarily for evaluation of template formatting keySchema: z
// prependPrefix: z .string()
// .string() .optional()
// .trim() .refine(
// .transform((str) => str.toUpperCase()) (val) => !val || /^(?:[a-zA-Z0-9\-/]*)(?:\{\{secretKey\}\})(?:[a-zA-Z0-9\-/]*)$/.test(val),
// .optional(), {
// appendSuffix: z message:
// .string() "Key schema must include {{secretKey}} and only contain letters, numbers, dashes, slashes, and the {{secretKey}} placeholder."
// .trim() }
// .transform((str) => str.toUpperCase()) )
// .optional()
}); });
const syncOptionsSchema = additionalSyncOptions const syncOptionsSchema = additionalSyncOptions
@@ -86,10 +86,12 @@ export const useTriggerSecretSyncImportSecrets = () => {
mutationFn: async ({ mutationFn: async ({
syncId, syncId,
destination, destination,
importBehavior importBehavior,
filterForSchema,
stripSchema
}: TTriggerSecretSyncImportSecretsDTO) => { }: TTriggerSecretSyncImportSecretsDTO) => {
const { data } = await apiRequest.post( const { data } = await apiRequest.post(
`/api/v1/secret-syncs/${destination}/${syncId}/import-secrets?importBehavior=${importBehavior}` `/api/v1/secret-syncs/${destination}/${syncId}/import-secrets?importBehavior=${importBehavior}&filterForSchema=${filterForSchema}&stripSchema=${stripSchema}`
); );
return data; return data;
@@ -82,6 +82,8 @@ export type TTriggerSecretSyncImportSecretsDTO = {
syncId: string; syncId: string;
importBehavior: SecretSyncImportBehavior; importBehavior: SecretSyncImportBehavior;
projectId: string; projectId: string;
filterForSchema: boolean;
stripSchema: boolean;
}; };
export type TTriggerSecretSyncRemoveSecretsDTO = { export type TTriggerSecretSyncRemoveSecretsDTO = {
@@ -4,8 +4,7 @@ import { SecretSyncInitialSyncBehavior, SecretSyncStatus } from "@app/hooks/api/
export type RootSyncOptions = { export type RootSyncOptions = {
initialSyncBehavior: SecretSyncInitialSyncBehavior; initialSyncBehavior: SecretSyncInitialSyncBehavior;
disableSecretDeletion?: boolean; disableSecretDeletion?: boolean;
// prependPrefix?: string; keySchema?: string;
// appendSuffix?: string;
}; };
export type TRootSecretSync = { export type TRootSecretSync = {
@@ -21,12 +21,7 @@ type Props = {
export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) => { export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) => {
const { const {
destination, destination,
syncOptions: { syncOptions: { initialSyncBehavior, disableSecretDeletion, keySchema }
// appendSuffix,
// prependPrefix,
initialSyncBehavior,
disableSecretDeletion
}
} = secretSync; } = secretSync;
let AdditionalSyncOptionsComponent: ReactNode; let AdditionalSyncOptionsComponent: ReactNode;
@@ -88,8 +83,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
<GenericFieldLabel label="Initial Sync Behavior"> <GenericFieldLabel label="Initial Sync Behavior">
{SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destination).name} {SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destination).name}
</GenericFieldLabel> </GenericFieldLabel>
{/* <SecretSyncLabel label="Prefix">{prependPrefix}</SecretSyncLabel> <GenericFieldLabel label="Key Schema">{keySchema}</GenericFieldLabel>
<SecretSyncLabel label="Suffix">{appendSuffix}</SecretSyncLabel> */}
{AdditionalSyncOptionsComponent} {AdditionalSyncOptionsComponent}
{disableSecretDeletion && ( {disableSecretDeletion && (
<GenericFieldLabel label="Secret Deletion"> <GenericFieldLabel label="Secret Deletion">