improvements: final adjustments/improvements

This commit is contained in:
Scott Wilson
2025-01-21 20:21:29 -08:00
parent 3c1fc024c2
commit 99ca9e04f8
22 changed files with 255 additions and 155 deletions
+3
View File
@@ -1679,6 +1679,9 @@ export const SecretSyncs = {
const destinationName = SECRET_SYNC_NAME_MAP[destination]; const destinationName = SECRET_SYNC_NAME_MAP[destination];
return { return {
syncId: `The ID of the ${destinationName} Sync to be updated.`, syncId: `The ID of the ${destinationName} Sync to be updated.`,
connectionId: `The updated ID of the ${
APP_CONNECTION_NAME_MAP[SECRET_SYNC_CONNECTION_MAP[destination]]
} Connection to use for syncing.`,
name: `The updated name of the ${destinationName} Sync. Must be slug-friendly.`, name: `The updated name of the ${destinationName} Sync. Must be slug-friendly.`,
environment: `The updated slug of the project environment to sync secrets from.`, environment: `The updated slug of the project environment to sync secrets from.`,
secretPath: `The updated folder path to sync secrets from.`, secretPath: `The updated folder path to sync secrets from.`,
@@ -31,6 +31,7 @@ export const registerSyncSecretsEndpoints = <T extends TSecretSync, I extends TS
isEnabled?: boolean; isEnabled?: boolean;
}>; }>;
updateSchema: z.ZodType<{ updateSchema: z.ZodType<{
connectionId?: string;
name?: string; name?: string;
environment?: string; environment?: string;
secretPath?: string; secretPath?: string;
@@ -129,7 +129,7 @@ const deleteParametersBatch = async (
}; };
export const AwsParameterStoreSyncFns = { export const AwsParameterStoreSyncFns = {
syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, affixedSecretMap: TSecretMap) => { syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => {
const { destinationConfig } = secretSync; const { destinationConfig } = secretSync;
const ssm = await getSSM(secretSync); const ssm = await getSSM(secretSync);
@@ -138,7 +138,7 @@ export const AwsParameterStoreSyncFns = {
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path); const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
for await (const entry of Object.entries(affixedSecretMap)) { for await (const entry of Object.entries(secretMap)) {
const [key, { value }] = entry; const [key, { value }] = entry;
// skip empty values (not allowed by AWS) or secrets that haven't changed // skip empty values (not allowed by AWS) or secrets that haven't changed
@@ -167,7 +167,7 @@ export const AwsParameterStoreSyncFns = {
for (const entry of Object.entries(awsParameterStoreSecretsRecord)) { for (const entry of Object.entries(awsParameterStoreSecretsRecord)) {
const [key, parameter] = entry; const [key, parameter] = entry;
if (!(key in affixedSecretMap) || !affixedSecretMap[key].value) { if (!(key in secretMap) || !secretMap[key].value) {
parametersToDelete.push(parameter); parametersToDelete.push(parameter);
} }
} }
@@ -185,7 +185,7 @@ export const AwsParameterStoreSyncFns = {
Object.entries(awsParameterStoreSecretsRecord).map(([key, value]) => [key, { value: value.Value ?? "" }]) Object.entries(awsParameterStoreSecretsRecord).map(([key, value]) => [key, { value: value.Value ?? "" }])
); );
}, },
removeSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, affixedSecretMap: TSecretMap) => { removeSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => {
const { destinationConfig } = secretSync; const { destinationConfig } = secretSync;
const ssm = await getSSM(secretSync); const ssm = await getSSM(secretSync);
@@ -197,7 +197,7 @@ export const AwsParameterStoreSyncFns = {
for (const entry of Object.entries(awsParameterStoreSecretsRecord)) { for (const entry of Object.entries(awsParameterStoreSecretsRecord)) {
const [key, param] = entry; const [key, param] = entry;
if (key in affixedSecretMap) { if (key in secretMap) {
parametersToDelete.push(param); parametersToDelete.push(param);
} }
} }
@@ -3,6 +3,7 @@ import sodium from "libsodium-wrappers";
import { getGitHubClient } from "@app/services/app-connection/github"; import { getGitHubClient } from "@app/services/app-connection/github";
import { GitHubSyncScope, GitHubSyncVisibility } from "@app/services/secret-sync/github/github-sync-enums"; import { GitHubSyncScope, GitHubSyncVisibility } from "@app/services/secret-sync/github/github-sync-enums";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
@@ -158,7 +159,33 @@ const putSecret = async (client: Octokit, secretSync: TGitHubSyncWithCredentials
}; };
export const GithubSyncFns = { export const GithubSyncFns = {
syncSecrets: async (secretSync: TGitHubSyncWithCredentials, affixedSecretMap: TSecretMap) => { syncSecrets: async (secretSync: TGitHubSyncWithCredentials, secretMap: TSecretMap) => {
switch (secretSync.destinationConfig.scope) {
case GitHubSyncScope.Organization:
if (Object.values(secretMap).length > 1000) {
throw new SecretSyncError({
message: "GitHub does not support storing more than 1,000 secrets at the organization level.",
shouldRetry: false
});
}
break;
case GitHubSyncScope.Repository:
case GitHubSyncScope.RepositoryEnvironment:
if (Object.values(secretMap).length > 100) {
throw new SecretSyncError({
message: "GitHub does not support storing more than 100 secrets at the repository level.",
shouldRetry: false
});
}
break;
default:
throw new Error(
`Unsupported GitHub Sync scope ${
(secretSync.destinationConfig as TGitHubSyncWithCredentials["destinationConfig"]).scope
}`
);
}
const client = getGitHubClient(secretSync.connection); const client = getGitHubClient(secretSync.connection);
const encryptedSecrets = await getEncryptedSecrets(client, secretSync); const encryptedSecrets = await getEncryptedSecrets(client, secretSync);
@@ -166,16 +193,16 @@ export const GithubSyncFns = {
const publicKey = await getPublicKey(client, secretSync); const publicKey = await getPublicKey(client, secretSync);
for await (const encryptedSecret of encryptedSecrets) { for await (const encryptedSecret of encryptedSecrets) {
if (!(encryptedSecret.name in affixedSecretMap)) { if (!(encryptedSecret.name in secretMap)) {
await deleteSecret(client, secretSync, encryptedSecret); await deleteSecret(client, secretSync, encryptedSecret);
} }
} }
await sodium.ready.then(async () => { await sodium.ready.then(async () => {
for await (const key of Object.keys(affixedSecretMap)) { for await (const key of Object.keys(secretMap)) {
// convert secret & base64 key to Uint8Array. // convert secret & base64 key to Uint8Array.
const binaryKey = sodium.from_base64(publicKey.key, sodium.base64_variants.ORIGINAL); const binaryKey = sodium.from_base64(publicKey.key, sodium.base64_variants.ORIGINAL);
const binarySecretValue = sodium.from_string(affixedSecretMap[key].value); const binarySecretValue = sodium.from_string(secretMap[key].value);
// encrypt secret using libsodium // encrypt secret using libsodium
const encryptedBytes = sodium.crypto_box_seal(binarySecretValue, binaryKey); const encryptedBytes = sodium.crypto_box_seal(binarySecretValue, binaryKey);
@@ -183,24 +210,31 @@ export const GithubSyncFns = {
// convert encrypted Uint8Array to base64 // convert encrypted Uint8Array to base64
const encryptedSecretValue = sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL); const encryptedSecretValue = sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL);
await putSecret(client, secretSync, { try {
secret_name: key, await putSecret(client, secretSync, {
encrypted_value: encryptedSecretValue, secret_name: key,
key_id: publicKey.key_id encrypted_value: encryptedSecretValue,
}); key_id: publicKey.key_id
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
} }
}); });
}, },
getSecrets: async (secretSync: TGitHubSyncWithCredentials) => { getSecrets: async (secretSync: TGitHubSyncWithCredentials) => {
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
}, },
removeSecrets: async (secretSync: TGitHubSyncWithCredentials, affixedSecretMap: TSecretMap) => { removeSecrets: async (secretSync: TGitHubSyncWithCredentials, secretMap: TSecretMap) => {
const client = getGitHubClient(secretSync.connection); const client = getGitHubClient(secretSync.connection);
const encryptedSecrets = await getEncryptedSecrets(client, secretSync); const encryptedSecrets = await getEncryptedSecrets(client, secretSync);
for await (const encryptedSecret of encryptedSecrets) { for await (const encryptedSecret of encryptedSecrets) {
if (encryptedSecret.name in affixedSecretMap) { if (encryptedSecret.name in secretMap) {
await deleteSecret(client, secretSync, encryptedSecret); await deleteSecret(client, secretSync, encryptedSecret);
} }
} }
@@ -1,14 +1,23 @@
export class SecretSyncError extends Error { export class SecretSyncError extends Error {
name: string; name: string;
error: unknown; error?: unknown;
secretKey?: string; secretKey?: string;
constructor({ name, error, secretKey }: { name?: string; error?: unknown; secretKey?: string } = {}) { shouldRetry?: boolean;
super();
constructor({
name,
error,
secretKey,
message,
shouldRetry = true
}: { name?: string; error?: unknown; secretKey?: string; shouldRetry?: boolean; message?: string } = {}) {
super(message);
this.name = name || "SecretSyncError"; this.name = name || "SecretSyncError";
this.error = error; this.error = error;
this.secretKey = secretKey; this.secretKey = secretKey;
this.shouldRetry = shouldRetry;
} }
} }
@@ -22,55 +22,55 @@ export const listSecretSyncOptions = () => {
return Object.values(SECRET_SYNC_LIST_OPTIONS).sort((a, b) => a.name.localeCompare(b.name)); return Object.values(SECRET_SYNC_LIST_OPTIONS).sort((a, b) => a.name.localeCompare(b.name));
}; };
const addAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => { // const addAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => {
let secretMap = { ...unprocessedSecretMap }; // let secretMap = { ...unprocessedSecretMap };
//
const { appendSuffix, prependPrefix } = secretSync.syncOptions; // const { appendSuffix, prependPrefix } = secretSync.syncOptions;
//
if (appendSuffix || prependPrefix) { // if (appendSuffix || prependPrefix) {
secretMap = {}; // secretMap = {};
Object.entries(unprocessedSecretMap).forEach(([key, value]) => { // Object.entries(unprocessedSecretMap).forEach(([key, value]) => {
secretMap[`${prependPrefix || ""}${key}${appendSuffix || ""}`] = value; // secretMap[`${prependPrefix || ""}${key}${appendSuffix || ""}`] = value;
}); // });
} // }
//
return secretMap; // return secretMap;
}; // };
//
const stripAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => { // const stripAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => {
let secretMap = { ...unprocessedSecretMap }; // let secretMap = { ...unprocessedSecretMap };
//
const { appendSuffix, prependPrefix } = secretSync.syncOptions; // const { appendSuffix, prependPrefix } = secretSync.syncOptions;
//
if (appendSuffix || prependPrefix) { // if (appendSuffix || prependPrefix) {
secretMap = {}; // secretMap = {};
Object.entries(unprocessedSecretMap).forEach(([key, value]) => { // Object.entries(unprocessedSecretMap).forEach(([key, value]) => {
let processedKey = key; // let processedKey = key;
//
if (prependPrefix && processedKey.startsWith(prependPrefix)) { // if (prependPrefix && processedKey.startsWith(prependPrefix)) {
processedKey = processedKey.slice(prependPrefix.length); // processedKey = processedKey.slice(prependPrefix.length);
} // }
//
if (appendSuffix && processedKey.endsWith(appendSuffix)) { // if (appendSuffix && processedKey.endsWith(appendSuffix)) {
processedKey = processedKey.slice(0, -appendSuffix.length); // processedKey = processedKey.slice(0, -appendSuffix.length);
} // }
//
secretMap[processedKey] = value; // secretMap[processedKey] = value;
}); // });
} // }
//
return secretMap; // return secretMap;
}; // };
export const SecretSyncFns = { export const SecretSyncFns = {
syncSecrets: (secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap): Promise<void> => { syncSecrets: (secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap): Promise<void> => {
const affixedSecretMap = addAffixes(secretSync, secretMap); // const affixedSecretMap = addAffixes(secretSync, secretMap);
switch (secretSync.destination) { switch (secretSync.destination) {
case SecretSync.AWSParameterStore: case SecretSync.AWSParameterStore:
return AwsParameterStoreSyncFns.syncSecrets(secretSync, affixedSecretMap); return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap);
case SecretSync.GitHub: case SecretSync.GitHub:
return GithubSyncFns.syncSecrets(secretSync, affixedSecretMap); return GithubSyncFns.syncSecrets(secretSync, secretMap);
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for push secrets: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for push secrets: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -92,16 +92,17 @@ export const SecretSyncFns = {
); );
} }
return stripAffixes(secretSync, secretMap); return secretMap;
// return stripAffixes(secretSync, secretMap);
}, },
removeSecrets: (secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap): Promise<void> => { removeSecrets: (secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap): Promise<void> => {
const affixedSecretMap = addAffixes(secretSync, secretMap); // const affixedSecretMap = addAffixes(secretSync, secretMap);
switch (secretSync.destination) { switch (secretSync.destination) {
case SecretSync.AWSParameterStore: case SecretSync.AWSParameterStore:
return AwsParameterStoreSyncFns.removeSecrets(secretSync, affixedSecretMap); return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap);
case SecretSync.GitHub: case SecretSync.GitHub:
return GithubSyncFns.removeSecrets(secretSync, affixedSecretMap); return GithubSyncFns.removeSecrets(secretSync, secretMap);
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for removing secrets: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for removing secrets: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -114,7 +115,7 @@ export const parseSyncErrorMessage = (err: unknown): string => {
if (err instanceof SecretSyncError) { if (err instanceof SecretSyncError) {
return JSON.stringify({ return JSON.stringify({
secretKey: err.secretKey, secretKey: err.secretKey,
error: parseSyncErrorMessage(err.error) error: err.message ?? parseSyncErrorMessage(err.error)
}); });
} }
@@ -31,6 +31,7 @@ import {
SecretSyncImportBehavior, SecretSyncImportBehavior,
SecretSyncInitialSyncBehavior SecretSyncInitialSyncBehavior
} from "@app/services/secret-sync/secret-sync-enums"; } from "@app/services/secret-sync/secret-sync-enums";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { parseSyncErrorMessage, SecretSyncFns } from "@app/services/secret-sync/secret-sync-fns"; import { parseSyncErrorMessage, SecretSyncFns } from "@app/services/secret-sync/secret-sync-fns";
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
import { import {
@@ -94,6 +95,19 @@ type SecretSyncActionJob = Job<
TQueueSecretSyncSyncSecretsByIdDTO | TQueueSecretSyncImportSecretsByIdDTO | TQueueSecretSyncRemoveSecretsByIdDTO TQueueSecretSyncSyncSecretsByIdDTO | TQueueSecretSyncImportSecretsByIdDTO | TQueueSecretSyncRemoveSecretsByIdDTO
>; >;
const getRequeueDelay = (failureCount?: number) => {
if (!failureCount) return 0;
const baseDelay = 1000;
const maxDelay = 30000;
const delay = Math.min(baseDelay * 2 ** failureCount, maxDelay);
const jitter = delay * (0.5 + Math.random() * 0.5);
return jitter;
};
export const secretSyncQueueFactory = ({ export const secretSyncQueueFactory = ({
queueService, queueService,
kmsService, kmsService,
@@ -164,13 +178,18 @@ export const secretSyncQueueFactory = ({
resourceMetadataDAL resourceMetadataDAL
}); });
const $getSecrets = async (secretSync: TSecretSyncRaw | TSecretSyncWithCredentials, includeImports = true) => { const $getInfisicalSecrets = async (
secretSync: TSecretSyncRaw | TSecretSyncWithCredentials,
includeImports = true
) => {
const { projectId, folderId, environment, folder } = secretSync; const { projectId, folderId, environment, folder } = secretSync;
if (!folderId || !environment || !folder) if (!folderId || !environment || !folder)
throw new Error( throw new SecretSyncError({
"Invalid Secret Sync source configuration: folder no longer exists. Please update source environment and secret path." message:
); "Invalid Secret Sync source configuration: folder no longer exists. Please update source environment and secret path.",
shouldRetry: false
});
const secretMap: TSecretMap = {}; const secretMap: TSecretMap = {};
@@ -247,7 +266,7 @@ export const secretSyncQueueFactory = ({
const queueSecretSyncSyncSecretsById = async (payload: TQueueSecretSyncSyncSecretsByIdDTO) => const queueSecretSyncSyncSecretsById = async (payload: TQueueSecretSyncSyncSecretsByIdDTO) =>
queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.SecretSyncSyncSecrets, payload, { queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.SecretSyncSyncSecrets, payload, {
delay: payload.failedToAcquireLockCount ? 1000 : 0, // we don't want to delay initial job delay: getRequeueDelay(payload.failedToAcquireLockCount), // this is for delaying re-queued jobs if sync is locked
attempts: 5, attempts: 5,
backoff: { backoff: {
type: "exponential", type: "exponential",
@@ -309,7 +328,7 @@ export const secretSyncQueueFactory = ({
const importedSecretMap: TSecretMap = {}; const importedSecretMap: TSecretMap = {};
const secretMap = await $getSecrets(secretSync, false); const secretMap = await $getInfisicalSecrets(secretSync, false);
const secretsToCreate: Parameters<typeof $createManySecretsRawFn>[0]["secrets"] = []; const secretsToCreate: Parameters<typeof $createManySecretsRawFn>[0]["secrets"] = [];
const secretsToUpdate: Parameters<typeof $updateManySecretsRawFn>[0]["secrets"] = []; const secretsToUpdate: Parameters<typeof $updateManySecretsRawFn>[0]["secrets"] = [];
@@ -374,7 +393,7 @@ export const secretSyncQueueFactory = ({
let isSynced = false; let isSynced = false;
let syncMessage: string | null = null; let syncMessage: string | null = null;
const isFinalAttempt = job.attemptsStarted === job.opts.attempts; let isFinalAttempt = job.attemptsStarted === job.opts.attempts;
try { try {
const { const {
@@ -400,7 +419,7 @@ export const secretSyncQueueFactory = ({
syncOptions: { initialSyncBehavior } syncOptions: { initialSyncBehavior }
} = secretSyncWithCredentials; } = secretSyncWithCredentials;
const secretMap = await $getSecrets(secretSync); const secretMap = await $getInfisicalSecrets(secretSync);
if (!lastSyncedAt && initialSyncBehavior !== SecretSyncInitialSyncBehavior.OverwriteDestination) { if (!lastSyncedAt && initialSyncBehavior !== SecretSyncInitialSyncBehavior.OverwriteDestination) {
const importedSecretMap = await $importSecrets( const importedSecretMap = await $importSecrets(
@@ -438,8 +457,12 @@ export const secretSyncQueueFactory = ({
syncMessage = parseSyncErrorMessage(err); syncMessage = parseSyncErrorMessage(err);
// re-throw so job fails if (err instanceof SecretSyncError && !err.shouldRetry) {
throw err; isFinalAttempt = true;
} else {
// re-throw so job fails
throw err;
}
} finally { } finally {
const ranAt = new Date(); const ranAt = new Date();
const syncStatus = isSynced ? SecretSyncStatus.Succeeded : SecretSyncStatus.Failed; const syncStatus = isSynced ? SecretSyncStatus.Succeeded : SecretSyncStatus.Failed;
@@ -639,7 +662,7 @@ export const secretSyncQueueFactory = ({
kmsService kmsService
}); });
const secretMap = await $getSecrets(secretSync); const secretMap = await $getInfisicalSecrets(secretSync);
await SecretSyncFns.removeSecrets( await SecretSyncFns.removeSecrets(
{ {
@@ -816,7 +839,6 @@ export const secretSyncQueueFactory = ({
case QueueJobs.SecretSyncSyncSecrets: { case QueueJobs.SecretSyncSyncSecrets: {
const { failedToAcquireLockCount = 0, ...rest } = job.data as TQueueSecretSyncSyncSecretsByIdDTO; const { failedToAcquireLockCount = 0, ...rest } = job.data as TQueueSecretSyncSyncSecretsByIdDTO;
//
if (failedToAcquireLockCount < 10) { if (failedToAcquireLockCount < 10) {
await queueSecretSyncSyncSecretsById({ ...rest, failedToAcquireLockCount: failedToAcquireLockCount + 1 }); await queueSecretSyncSyncSecretsById({ ...rest, failedToAcquireLockCount: failedToAcquireLockCount + 1 });
return; return;
@@ -13,19 +13,19 @@ const SyncOptionsSchema = (secretSync: SecretSync, options: TSyncOptionsConfig =
initialSyncBehavior: (options.canImportSecrets initialSyncBehavior: (options.canImportSecrets
? z.nativeEnum(SecretSyncInitialSyncBehavior) ? z.nativeEnum(SecretSyncInitialSyncBehavior)
: z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination) : z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination)
).describe(SecretSyncs.SYNC_OPTIONS(secretSync).INITIAL_SYNC_BEHAVIOR), ).describe(SecretSyncs.SYNC_OPTIONS(secretSync).INITIAL_SYNC_BEHAVIOR)
prependPrefix: z // prependPrefix: z
.string() // .string()
.trim() // .trim()
.transform((str) => str.toUpperCase()) // .transform((str) => str.toUpperCase())
.optional() // .optional()
.describe(SecretSyncs.SYNC_OPTIONS(secretSync).PREPEND_PREFIX), // .describe(SecretSyncs.SYNC_OPTIONS(secretSync).PREPEND_PREFIX),
appendSuffix: z // appendSuffix: z
.string() // .string()
.trim() // .trim()
.transform((str) => str.toUpperCase()) // .transform((str) => str.toUpperCase())
.optional() // .optional()
.describe(SecretSyncs.SYNC_OPTIONS(secretSync).APPEND_SUFFIX) // .describe(SecretSyncs.SYNC_OPTIONS(secretSync).APPEND_SUFFIX)
}); });
export const BaseSecretSyncSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) => export const BaseSecretSyncSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) =>
@@ -72,6 +72,7 @@ export const GenericCreateSecretSyncFieldsSchema = (destination: SecretSync, syn
export const GenericUpdateSecretSyncFieldsSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) => export const GenericUpdateSecretSyncFieldsSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) =>
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(),
description: z description: z
.string() .string()
.trim() .trim()
@@ -13,6 +13,7 @@ import { OrgServiceActor } from "@app/lib/types";
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { listSecretSyncOptions } from "@app/services/secret-sync/secret-sync-fns"; import { listSecretSyncOptions } from "@app/services/secret-sync/secret-sync-fns";
import { import {
SecretSyncStatus, SecretSyncStatus,
@@ -262,6 +263,13 @@ export const secretSyncServiceFactory = ({
const updatedSecretSync = await secretSyncDAL.transaction(async (tx) => { const updatedSecretSync = await secretSyncDAL.transaction(async (tx) => {
let { folderId } = secretSync; let { folderId } = secretSync;
if (params.connectionId) {
const destinationApp = SECRET_SYNC_CONNECTION_MAP[secretSync.destination as SecretSync];
// validates permission to connect and app is valid for sync destination
await appConnectionService.connectAppConnectionById(destinationApp, params.connectionId, actor);
}
if ( if (
(secretPath && secretPath !== secretSync.folder?.path) || (secretPath && secretPath !== secretSync.folder?.path) ||
(environment && environment !== secretSync.environment?.slug) (environment && environment !== secretSync.environment?.slug)
@@ -54,7 +54,7 @@ export type TCreateSecretSyncDTO = Pick<TSecretSync, "syncOptions" | "destinatio
isEnabled?: boolean; isEnabled?: boolean;
}; };
export type TUpdateSecretSyncDTO = Partial<Omit<TCreateSecretSyncDTO, "connectionId" | "projectId">> & { export type TUpdateSecretSyncDTO = Partial<Omit<TCreateSecretSyncDTO, "projectId">> & {
syncId: string; syncId: string;
destination: SecretSync; destination: SecretSync;
}; };
@@ -40,8 +40,6 @@ 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 prior to syncing, prioritizing values present in Infisical if secrets conflict. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in Infisical if secrets conflict.
- **Import Secrets (Prioritize Parameter Store)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in Parameter Store if secrets conflict. - **Import Secrets (Prioritize Parameter Store)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in Parameter Store if secrets conflict.
- **Prepend Prefix**: An optional prefix to prepend to secret keys when synced.
- **Append Suffix**: An optional suffix to append to secret keys when synced.
- **Enabled**: If enabled, secrets will automatically be synced from the source. Disable to prevent syncing until enabled. - **Enabled**: If enabled, secrets will automatically be synced from the source. Disable to prevent syncing until enabled.
6. Configure the **Details** of your Parameter Store Sync, then click **Next**. 6. Configure the **Details** of your Parameter Store Sync, then click **Next**.
@@ -76,8 +74,7 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
"secretPath": "/my-secrets", "secretPath": "/my-secrets",
"isEnabled": true, "isEnabled": true,
"syncOptions": { "syncOptions": {
"initialSyncBehavior": "overwrite-destination", "initialSyncBehavior": "overwrite-destination"
"prependPrefix": "INF_",
}, },
"destinationConfig": { "destinationConfig": {
"region": "us-east-1", "region": "us-east-1",
@@ -113,8 +110,7 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
"lastRemoveMessage": null, "lastRemoveMessage": null,
"lastRemovedAt": null, "lastRemovedAt": null,
"syncOptions": { "syncOptions": {
"initialSyncBehavior": "overwrite-destination", "initialSyncBehavior": "overwrite-destination"
"prependPrefix": "INF_"
}, },
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"connection": { "connection": {
+4 -8
View File
@@ -62,8 +62,6 @@ description: "Learn how to configure a GitHub Sync for Infisical."
<Note> <Note>
GitHub does not support importing secrets. GitHub does not support importing secrets.
</Note> </Note>
- **Prepend Prefix**: An optional prefix to prepend to secret keys when synced.
- **Append Suffix**: An optional suffix to append to secret keys when synced.
- **Enabled**: If enabled, secrets will automatically be synced from the source. Disable to prevent syncing until enabled. - **Enabled**: If enabled, secrets will automatically be synced from the source. Disable to prevent syncing until enabled.
6. Configure the **Details** of your GitHub Sync, then click **Next**. 6. Configure the **Details** of your GitHub Sync, then click **Next**.
@@ -97,13 +95,12 @@ description: "Learn how to configure a GitHub Sync for Infisical."
"secretPath": "/my-secrets", "secretPath": "/my-secrets",
"isEnabled": true, "isEnabled": true,
"syncOptions": { "syncOptions": {
"initialSyncBehavior": "overwrite-destination", "initialSyncBehavior": "overwrite-destination"
"prependPrefix": "INF_",
}, },
"destinationConfig": { "destinationConfig": {
"scope": "repository", "scope": "repository",
"owner": "my-github", "owner": "my-github",
"repo: "my-repository" "repo": "my-repository"
} }
}' }'
``` ```
@@ -135,8 +132,7 @@ description: "Learn how to configure a GitHub Sync for Infisical."
"lastRemoveMessage": null, "lastRemoveMessage": null,
"lastRemovedAt": null, "lastRemovedAt": null,
"syncOptions": { "syncOptions": {
"initialSyncBehavior": "overwrite-destination", "initialSyncBehavior": "overwrite-destination"
"prependPrefix": "INF_"
}, },
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"connection": { "connection": {
@@ -157,7 +153,7 @@ description: "Learn how to configure a GitHub Sync for Infisical."
"destinationConfig": { "destinationConfig": {
"scope": "repository", "scope": "repository",
"owner": "my-github", "owner": "my-github",
"repo: "my-repository" "repo": "my-repository"
} }
} }
} }
@@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Switch } from "@app/components/v2"; import { Button, Checkbox, FormControl, Switch } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { import {
@@ -43,6 +43,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
const { name: destinationName } = SECRET_SYNC_MAP[destination]; const { name: destinationName } = SECRET_SYNC_MAP[destination];
const [selectedTabIndex, setSelectedTabIndex] = useState(0); const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const [confirmOverwrite, setConfirmOverwrite] = useState(false);
const { syncOption } = useSecretSyncOption(destination); const { syncOption } = useSecretSyncOption(destination);
@@ -93,7 +94,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
setSelectedTabIndex((prev) => prev - 1); setSelectedTabIndex((prev) => prev - 1);
}; };
const { handleSubmit, trigger } = formMethods; const { handleSubmit, trigger, watch, control } = formMethods;
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields); const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
@@ -122,7 +123,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
return isEnabled; return isEnabled;
}; };
const { control } = formMethods; const initialSyncBehavior = watch("syncOptions.initialSyncBehavior");
return ( return (
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}> <form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}>
@@ -195,10 +196,33 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
</Tab.Panels> </Tab.Panels>
</Tab.Group> </Tab.Group>
</FormProvider> </FormProvider>
{isFinalStep &&
initialSyncBehavior === SecretSyncInitialSyncBehavior.OverwriteDestination && (
<Checkbox
id="confirm-overwrite"
isChecked={confirmOverwrite}
containerClassName="-mt-5"
onCheckedChange={(isChecked) => setConfirmOverwrite(Boolean(isChecked))}
>
<p className={`mt-5 text-wrap ${confirmOverwrite ? "text-mineshaft-200" : "text-red"}`}>
I understand all secrets present in the configured {destinationName} destination will
be removed that are not present within Infisical.
</p>
</Checkbox>
)}
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4"> <div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
<Button onClick={handleNext} colorSchema="secondary"> <Button
isDisabled={
isFinalStep &&
initialSyncBehavior === SecretSyncInitialSyncBehavior.OverwriteDestination &&
!confirmOverwrite
}
onClick={handleNext}
colorSchema="secondary"
>
{isFinalStep ? "Create Sync" : "Next"} {isFinalStep ? "Create Sync" : "Next"}
</Button> </Button>
{}
{selectedTabIndex > 0 && ( {selectedTabIndex > 0 && (
<Button onClick={handlePrev} colorSchema="secondary"> <Button onClick={handlePrev} colorSchema="secondary">
Back Back
@@ -35,12 +35,13 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) =>
reValidateMode: "onChange" reValidateMode: "onChange"
}); });
const onSubmit = async ({ environment, ...formData }: TSecretSyncForm) => { const onSubmit = async ({ environment, connection, ...formData }: TSecretSyncForm) => {
try { try {
const updatedSecretSync = await updateSecretSync.mutateAsync({ const updatedSecretSync = await updateSecretSync.mutateAsync({
syncId: secretSync.id, syncId: secretSync.id,
...formData, ...formData,
environment: environment?.slug environment: environment?.slug,
connectionId: connection.id
}); });
createNotification({ createNotification({
@@ -2,7 +2,7 @@ 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, Input, 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 { useSecretSyncOption } from "@app/hooks/api/secretSyncs";
@@ -91,7 +91,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
)} )}
</> </>
)} )}
<Controller {/* <Controller
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl <FormControl
isError={Boolean(error)} isError={Boolean(error)}
@@ -118,7 +118,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
)} )}
control={control} control={control}
name="syncOptions.appendSuffix" name="syncOptions.appendSuffix"
/> /> */}
</> </>
); );
}; };
@@ -21,7 +21,10 @@ export const SecretSyncReviewFields = () => {
connection, connection,
environment, environment,
secretPath, secretPath,
syncOptions: { appendSuffix, prependPrefix, initialSyncBehavior }, syncOptions: {
// appendSuffix, prependPrefix,
initialSyncBehavior
},
destination, destination,
isEnabled isEnabled
} = watch(); } = watch();
@@ -72,8 +75,8 @@ export const SecretSyncReviewFields = () => {
<SecretSyncLabel label="Initial Sync Behavior"> <SecretSyncLabel label="Initial Sync Behavior">
{SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destinationName).name} {SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destinationName).name}
</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> */}
</div> </div>
</div> </div>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
@@ -13,17 +13,18 @@ const BaseSecretSyncSchema = z.object({
environment: z.object({ slug: z.string(), id: z.string(), name: z.string() }), environment: z.object({ slug: z.string(), id: z.string(), name: z.string() }),
secretPath: z.string().min(1, "Secret path required"), secretPath: z.string().min(1, "Secret path required"),
syncOptions: z.object({ syncOptions: z.object({
initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior), initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior)
prependPrefix: z // scott: removed temporarily for evaluation of template formatting
.string() // prependPrefix: z
.trim() // .string()
.transform((str) => str.toUpperCase()) // .trim()
.optional(), // .transform((str) => str.toUpperCase())
appendSuffix: z // .optional(),
.string() // appendSuffix: z
.trim() // .string()
.transform((str) => str.toUpperCase()) // .trim()
.optional() // .transform((str) => str.toUpperCase())
// .optional()
}), }),
isEnabled: z.boolean() isEnabled: z.boolean()
}); });
@@ -28,7 +28,7 @@ export type TCreateSecretSyncDTO = DiscriminativePick<
> & { environment: string; secretPath: string; projectId: string }; > & { environment: string; secretPath: string; projectId: string };
export type TUpdateSecretSyncDTO = Partial< export type TUpdateSecretSyncDTO = Partial<
Omit<TCreateSecretSyncDTO, "connectionId" | "destination" | "projectId"> Omit<TCreateSecretSyncDTO, "destination" | "projectId">
> & { > & {
destination: SecretSync; destination: SecretSync;
syncId: string; syncId: string;
@@ -26,8 +26,8 @@ export type TRootSecretSync = {
lastRemoveMessage: string | null; lastRemoveMessage: string | null;
syncOptions: { syncOptions: {
initialSyncBehavior: SecretSyncInitialSyncBehavior; initialSyncBehavior: SecretSyncInitialSyncBehavior;
prependPrefix?: string; // prependPrefix?: string;
appendSuffix?: string; // appendSuffix?: string;
}; };
connection: { connection: {
app: AppConnection; app: AppConnection;
@@ -19,7 +19,7 @@ export const SecretSyncsTab = () => {
const { data: secretSyncs = [], isPending: isSecretSyncsPending } = useListSecretSyncs( const { data: secretSyncs = [], isPending: isSecretSyncsPending } = useListSecretSyncs(
currentWorkspace.id, currentWorkspace.id,
{ {
refetchInterval: 2000 refetchInterval: 4000
} }
); );
@@ -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);
@@ -109,7 +109,10 @@ 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 secretSync={secretSync} onEditOptions={handleEditOptions} /> <SecretSyncOptionsSection
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,24 +1,23 @@
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 { 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 { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP } from "@app/helpers/secretSyncs";
import { TSecretSync } from "@app/hooks/api/secretSyncs"; import { TSecretSync } from "@app/hooks/api/secretSyncs";
type Props = { type Props = {
secretSync: TSecretSync; secretSync: TSecretSync;
onEditOptions: VoidFunction; // onEditOptions: VoidFunction;
}; };
export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) => { export const SecretSyncOptionsSection = ({
secretSync
// onEditOptions
}: Props) => {
const { const {
destination, destination,
syncOptions: { appendSuffix, prependPrefix, initialSyncBehavior }, syncOptions: {
lastSyncedAt // appendSuffix,
// prependPrefix,
initialSyncBehavior
}
} = secretSync; } = secretSync;
return ( return (
@@ -26,7 +25,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
<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 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"> <div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="font-semibold text-mineshaft-100">Sync Options</h3> <h3 className="font-semibold text-mineshaft-100">Sync Options</h3>
<ProjectPermissionCan {/* <ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.Edit} I={ProjectPermissionSecretSyncActions.Edit}
a={ProjectPermissionSub.SecretSyncs} a={ProjectPermissionSub.SecretSyncs}
> >
@@ -41,17 +40,15 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
<FontAwesomeIcon icon={faEdit} /> <FontAwesomeIcon icon={faEdit} />
</IconButton> </IconButton>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan> */}
</div> </div>
<div> <div>
<div className="space-y-3"> <div className="space-y-3">
{!lastSyncedAt && ( <SecretSyncLabel label="Initial Sync Behavior">
<SecretSyncLabel label="Initial Sync Behavior"> {SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destination).name}
{SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destination).name} </SecretSyncLabel>
</SecretSyncLabel> {/* <SecretSyncLabel label="Prefix">{prependPrefix}</SecretSyncLabel>
)} <SecretSyncLabel label="Suffix">{appendSuffix}</SecretSyncLabel> */}
<SecretSyncLabel label="Prefix">{prependPrefix}</SecretSyncLabel>
<SecretSyncLabel label="Suffix">{appendSuffix}</SecretSyncLabel>
</div> </div>
</div> </div>
</div> </div>