mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
improvements: final adjustments/improvements
This commit is contained in:
@@ -1679,6 +1679,9 @@ export const SecretSyncs = {
|
||||
const destinationName = SECRET_SYNC_NAME_MAP[destination];
|
||||
return {
|
||||
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.`,
|
||||
environment: `The updated slug of the project environment 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;
|
||||
}>;
|
||||
updateSchema: z.ZodType<{
|
||||
connectionId?: string;
|
||||
name?: string;
|
||||
environment?: string;
|
||||
secretPath?: string;
|
||||
|
||||
@@ -129,7 +129,7 @@ const deleteParametersBatch = async (
|
||||
};
|
||||
|
||||
export const AwsParameterStoreSyncFns = {
|
||||
syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, affixedSecretMap: TSecretMap) => {
|
||||
syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { destinationConfig } = secretSync;
|
||||
|
||||
const ssm = await getSSM(secretSync);
|
||||
@@ -138,7 +138,7 @@ export const AwsParameterStoreSyncFns = {
|
||||
|
||||
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;
|
||||
|
||||
// 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)) {
|
||||
const [key, parameter] = entry;
|
||||
|
||||
if (!(key in affixedSecretMap) || !affixedSecretMap[key].value) {
|
||||
if (!(key in secretMap) || !secretMap[key].value) {
|
||||
parametersToDelete.push(parameter);
|
||||
}
|
||||
}
|
||||
@@ -185,7 +185,7 @@ export const AwsParameterStoreSyncFns = {
|
||||
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 ssm = await getSSM(secretSync);
|
||||
@@ -197,7 +197,7 @@ export const AwsParameterStoreSyncFns = {
|
||||
for (const entry of Object.entries(awsParameterStoreSecretsRecord)) {
|
||||
const [key, param] = entry;
|
||||
|
||||
if (key in affixedSecretMap) {
|
||||
if (key in secretMap) {
|
||||
parametersToDelete.push(param);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import sodium from "libsodium-wrappers";
|
||||
|
||||
import { getGitHubClient } from "@app/services/app-connection/github";
|
||||
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 { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
@@ -158,7 +159,33 @@ const putSecret = async (client: Octokit, secretSync: TGitHubSyncWithCredentials
|
||||
};
|
||||
|
||||
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 encryptedSecrets = await getEncryptedSecrets(client, secretSync);
|
||||
@@ -166,16 +193,16 @@ export const GithubSyncFns = {
|
||||
const publicKey = await getPublicKey(client, secretSync);
|
||||
|
||||
for await (const encryptedSecret of encryptedSecrets) {
|
||||
if (!(encryptedSecret.name in affixedSecretMap)) {
|
||||
if (!(encryptedSecret.name in secretMap)) {
|
||||
await deleteSecret(client, secretSync, encryptedSecret);
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
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
|
||||
const encryptedBytes = sodium.crypto_box_seal(binarySecretValue, binaryKey);
|
||||
@@ -183,24 +210,31 @@ export const GithubSyncFns = {
|
||||
// convert encrypted Uint8Array to base64
|
||||
const encryptedSecretValue = sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL);
|
||||
|
||||
await putSecret(client, secretSync, {
|
||||
secret_name: key,
|
||||
encrypted_value: encryptedSecretValue,
|
||||
key_id: publicKey.key_id
|
||||
});
|
||||
try {
|
||||
await putSecret(client, secretSync, {
|
||||
secret_name: key,
|
||||
encrypted_value: encryptedSecretValue,
|
||||
key_id: publicKey.key_id
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
getSecrets: async (secretSync: TGitHubSyncWithCredentials) => {
|
||||
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 encryptedSecrets = await getEncryptedSecrets(client, secretSync);
|
||||
|
||||
for await (const encryptedSecret of encryptedSecrets) {
|
||||
if (encryptedSecret.name in affixedSecretMap) {
|
||||
if (encryptedSecret.name in secretMap) {
|
||||
await deleteSecret(client, secretSync, encryptedSecret);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
export class SecretSyncError extends Error {
|
||||
name: string;
|
||||
|
||||
error: unknown;
|
||||
error?: unknown;
|
||||
|
||||
secretKey?: string;
|
||||
|
||||
constructor({ name, error, secretKey }: { name?: string; error?: unknown; secretKey?: string } = {}) {
|
||||
super();
|
||||
shouldRetry?: boolean;
|
||||
|
||||
constructor({
|
||||
name,
|
||||
error,
|
||||
secretKey,
|
||||
message,
|
||||
shouldRetry = true
|
||||
}: { name?: string; error?: unknown; secretKey?: string; shouldRetry?: boolean; message?: string } = {}) {
|
||||
super(message);
|
||||
this.name = name || "SecretSyncError";
|
||||
this.error = error;
|
||||
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));
|
||||
};
|
||||
|
||||
const addAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => {
|
||||
let secretMap = { ...unprocessedSecretMap };
|
||||
|
||||
const { appendSuffix, prependPrefix } = secretSync.syncOptions;
|
||||
|
||||
if (appendSuffix || prependPrefix) {
|
||||
secretMap = {};
|
||||
Object.entries(unprocessedSecretMap).forEach(([key, value]) => {
|
||||
secretMap[`${prependPrefix || ""}${key}${appendSuffix || ""}`] = value;
|
||||
});
|
||||
}
|
||||
|
||||
return secretMap;
|
||||
};
|
||||
|
||||
const stripAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => {
|
||||
let secretMap = { ...unprocessedSecretMap };
|
||||
|
||||
const { appendSuffix, prependPrefix } = secretSync.syncOptions;
|
||||
|
||||
if (appendSuffix || prependPrefix) {
|
||||
secretMap = {};
|
||||
Object.entries(unprocessedSecretMap).forEach(([key, value]) => {
|
||||
let processedKey = key;
|
||||
|
||||
if (prependPrefix && processedKey.startsWith(prependPrefix)) {
|
||||
processedKey = processedKey.slice(prependPrefix.length);
|
||||
}
|
||||
|
||||
if (appendSuffix && processedKey.endsWith(appendSuffix)) {
|
||||
processedKey = processedKey.slice(0, -appendSuffix.length);
|
||||
}
|
||||
|
||||
secretMap[processedKey] = value;
|
||||
});
|
||||
}
|
||||
|
||||
return secretMap;
|
||||
};
|
||||
// const addAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => {
|
||||
// let secretMap = { ...unprocessedSecretMap };
|
||||
//
|
||||
// const { appendSuffix, prependPrefix } = secretSync.syncOptions;
|
||||
//
|
||||
// if (appendSuffix || prependPrefix) {
|
||||
// secretMap = {};
|
||||
// Object.entries(unprocessedSecretMap).forEach(([key, value]) => {
|
||||
// secretMap[`${prependPrefix || ""}${key}${appendSuffix || ""}`] = value;
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// return secretMap;
|
||||
// };
|
||||
//
|
||||
// const stripAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => {
|
||||
// let secretMap = { ...unprocessedSecretMap };
|
||||
//
|
||||
// const { appendSuffix, prependPrefix } = secretSync.syncOptions;
|
||||
//
|
||||
// if (appendSuffix || prependPrefix) {
|
||||
// secretMap = {};
|
||||
// Object.entries(unprocessedSecretMap).forEach(([key, value]) => {
|
||||
// let processedKey = key;
|
||||
//
|
||||
// if (prependPrefix && processedKey.startsWith(prependPrefix)) {
|
||||
// processedKey = processedKey.slice(prependPrefix.length);
|
||||
// }
|
||||
//
|
||||
// if (appendSuffix && processedKey.endsWith(appendSuffix)) {
|
||||
// processedKey = processedKey.slice(0, -appendSuffix.length);
|
||||
// }
|
||||
//
|
||||
// secretMap[processedKey] = value;
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// return secretMap;
|
||||
// };
|
||||
|
||||
export const SecretSyncFns = {
|
||||
syncSecrets: (secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap): Promise<void> => {
|
||||
const affixedSecretMap = addAffixes(secretSync, secretMap);
|
||||
// const affixedSecretMap = addAffixes(secretSync, secretMap);
|
||||
|
||||
switch (secretSync.destination) {
|
||||
case SecretSync.AWSParameterStore:
|
||||
return AwsParameterStoreSyncFns.syncSecrets(secretSync, affixedSecretMap);
|
||||
return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.GitHub:
|
||||
return GithubSyncFns.syncSecrets(secretSync, affixedSecretMap);
|
||||
return GithubSyncFns.syncSecrets(secretSync, secretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`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> => {
|
||||
const affixedSecretMap = addAffixes(secretSync, secretMap);
|
||||
// const affixedSecretMap = addAffixes(secretSync, secretMap);
|
||||
|
||||
switch (secretSync.destination) {
|
||||
case SecretSync.AWSParameterStore:
|
||||
return AwsParameterStoreSyncFns.removeSecrets(secretSync, affixedSecretMap);
|
||||
return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.GitHub:
|
||||
return GithubSyncFns.removeSecrets(secretSync, affixedSecretMap);
|
||||
return GithubSyncFns.removeSecrets(secretSync, secretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for removing secrets: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -114,7 +115,7 @@ export const parseSyncErrorMessage = (err: unknown): string => {
|
||||
if (err instanceof SecretSyncError) {
|
||||
return JSON.stringify({
|
||||
secretKey: err.secretKey,
|
||||
error: parseSyncErrorMessage(err.error)
|
||||
error: err.message ?? parseSyncErrorMessage(err.error)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
SecretSyncImportBehavior,
|
||||
SecretSyncInitialSyncBehavior
|
||||
} 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 { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
|
||||
import {
|
||||
@@ -94,6 +95,19 @@ type SecretSyncActionJob = Job<
|
||||
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 = ({
|
||||
queueService,
|
||||
kmsService,
|
||||
@@ -164,13 +178,18 @@ export const secretSyncQueueFactory = ({
|
||||
resourceMetadataDAL
|
||||
});
|
||||
|
||||
const $getSecrets = async (secretSync: TSecretSyncRaw | TSecretSyncWithCredentials, includeImports = true) => {
|
||||
const $getInfisicalSecrets = async (
|
||||
secretSync: TSecretSyncRaw | TSecretSyncWithCredentials,
|
||||
includeImports = true
|
||||
) => {
|
||||
const { projectId, folderId, environment, folder } = secretSync;
|
||||
|
||||
if (!folderId || !environment || !folder)
|
||||
throw new Error(
|
||||
"Invalid Secret Sync source configuration: folder no longer exists. Please update source environment and secret path."
|
||||
);
|
||||
throw new SecretSyncError({
|
||||
message:
|
||||
"Invalid Secret Sync source configuration: folder no longer exists. Please update source environment and secret path.",
|
||||
shouldRetry: false
|
||||
});
|
||||
|
||||
const secretMap: TSecretMap = {};
|
||||
|
||||
@@ -247,7 +266,7 @@ export const secretSyncQueueFactory = ({
|
||||
|
||||
const queueSecretSyncSyncSecretsById = async (payload: TQueueSecretSyncSyncSecretsByIdDTO) =>
|
||||
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,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
@@ -309,7 +328,7 @@ export const secretSyncQueueFactory = ({
|
||||
|
||||
const importedSecretMap: TSecretMap = {};
|
||||
|
||||
const secretMap = await $getSecrets(secretSync, false);
|
||||
const secretMap = await $getInfisicalSecrets(secretSync, false);
|
||||
|
||||
const secretsToCreate: Parameters<typeof $createManySecretsRawFn>[0]["secrets"] = [];
|
||||
const secretsToUpdate: Parameters<typeof $updateManySecretsRawFn>[0]["secrets"] = [];
|
||||
@@ -374,7 +393,7 @@ export const secretSyncQueueFactory = ({
|
||||
|
||||
let isSynced = false;
|
||||
let syncMessage: string | null = null;
|
||||
const isFinalAttempt = job.attemptsStarted === job.opts.attempts;
|
||||
let isFinalAttempt = job.attemptsStarted === job.opts.attempts;
|
||||
|
||||
try {
|
||||
const {
|
||||
@@ -400,7 +419,7 @@ export const secretSyncQueueFactory = ({
|
||||
syncOptions: { initialSyncBehavior }
|
||||
} = secretSyncWithCredentials;
|
||||
|
||||
const secretMap = await $getSecrets(secretSync);
|
||||
const secretMap = await $getInfisicalSecrets(secretSync);
|
||||
|
||||
if (!lastSyncedAt && initialSyncBehavior !== SecretSyncInitialSyncBehavior.OverwriteDestination) {
|
||||
const importedSecretMap = await $importSecrets(
|
||||
@@ -438,8 +457,12 @@ export const secretSyncQueueFactory = ({
|
||||
|
||||
syncMessage = parseSyncErrorMessage(err);
|
||||
|
||||
// re-throw so job fails
|
||||
throw err;
|
||||
if (err instanceof SecretSyncError && !err.shouldRetry) {
|
||||
isFinalAttempt = true;
|
||||
} else {
|
||||
// re-throw so job fails
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
const ranAt = new Date();
|
||||
const syncStatus = isSynced ? SecretSyncStatus.Succeeded : SecretSyncStatus.Failed;
|
||||
@@ -639,7 +662,7 @@ export const secretSyncQueueFactory = ({
|
||||
kmsService
|
||||
});
|
||||
|
||||
const secretMap = await $getSecrets(secretSync);
|
||||
const secretMap = await $getInfisicalSecrets(secretSync);
|
||||
|
||||
await SecretSyncFns.removeSecrets(
|
||||
{
|
||||
@@ -816,7 +839,6 @@ export const secretSyncQueueFactory = ({
|
||||
case QueueJobs.SecretSyncSyncSecrets: {
|
||||
const { failedToAcquireLockCount = 0, ...rest } = job.data as TQueueSecretSyncSyncSecretsByIdDTO;
|
||||
|
||||
//
|
||||
if (failedToAcquireLockCount < 10) {
|
||||
await queueSecretSyncSyncSecretsById({ ...rest, failedToAcquireLockCount: failedToAcquireLockCount + 1 });
|
||||
return;
|
||||
|
||||
@@ -13,19 +13,19 @@ const SyncOptionsSchema = (secretSync: SecretSync, options: TSyncOptionsConfig =
|
||||
initialSyncBehavior: (options.canImportSecrets
|
||||
? z.nativeEnum(SecretSyncInitialSyncBehavior)
|
||||
: z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination)
|
||||
).describe(SecretSyncs.SYNC_OPTIONS(secretSync).INITIAL_SYNC_BEHAVIOR),
|
||||
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(secretSync).INITIAL_SYNC_BEHAVIOR)
|
||||
// prependPrefix: z
|
||||
// .string()
|
||||
// .trim()
|
||||
// .transform((str) => str.toUpperCase())
|
||||
// .optional()
|
||||
// .describe(SecretSyncs.SYNC_OPTIONS(secretSync).PREPEND_PREFIX),
|
||||
// appendSuffix: z
|
||||
// .string()
|
||||
// .trim()
|
||||
// .transform((str) => str.toUpperCase())
|
||||
// .optional()
|
||||
// .describe(SecretSyncs.SYNC_OPTIONS(secretSync).APPEND_SUFFIX)
|
||||
});
|
||||
|
||||
export const BaseSecretSyncSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) =>
|
||||
@@ -72,6 +72,7 @@ export const GenericCreateSecretSyncFieldsSchema = (destination: SecretSync, syn
|
||||
export const GenericUpdateSecretSyncFieldsSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "name" }).describe(SecretSyncs.UPDATE(destination).name).optional(),
|
||||
connectionId: z.string().uuid().describe(SecretSyncs.UPDATE(destination).connectionId).optional(),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
|
||||
@@ -13,6 +13,7 @@ import { OrgServiceActor } from "@app/lib/types";
|
||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
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 {
|
||||
SecretSyncStatus,
|
||||
@@ -262,6 +263,13 @@ export const secretSyncServiceFactory = ({
|
||||
const updatedSecretSync = await secretSyncDAL.transaction(async (tx) => {
|
||||
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 (
|
||||
(secretPath && secretPath !== secretSync.folder?.path) ||
|
||||
(environment && environment !== secretSync.environment?.slug)
|
||||
|
||||
@@ -54,7 +54,7 @@ export type TCreateSecretSyncDTO = Pick<TSecretSync, "syncOptions" | "destinatio
|
||||
isEnabled?: boolean;
|
||||
};
|
||||
|
||||
export type TUpdateSecretSyncDTO = Partial<Omit<TCreateSecretSyncDTO, "connectionId" | "projectId">> & {
|
||||
export type TUpdateSecretSyncDTO = Partial<Omit<TCreateSecretSyncDTO, "projectId">> & {
|
||||
syncId: string;
|
||||
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.
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
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",
|
||||
"isEnabled": true,
|
||||
"syncOptions": {
|
||||
"initialSyncBehavior": "overwrite-destination",
|
||||
"prependPrefix": "INF_",
|
||||
"initialSyncBehavior": "overwrite-destination"
|
||||
},
|
||||
"destinationConfig": {
|
||||
"region": "us-east-1",
|
||||
@@ -113,8 +110,7 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
|
||||
"lastRemoveMessage": null,
|
||||
"lastRemovedAt": null,
|
||||
"syncOptions": {
|
||||
"initialSyncBehavior": "overwrite-destination",
|
||||
"prependPrefix": "INF_"
|
||||
"initialSyncBehavior": "overwrite-destination"
|
||||
},
|
||||
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"connection": {
|
||||
|
||||
@@ -62,8 +62,6 @@ description: "Learn how to configure a GitHub Sync for Infisical."
|
||||
<Note>
|
||||
GitHub does not support importing secrets.
|
||||
</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.
|
||||
|
||||
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",
|
||||
"isEnabled": true,
|
||||
"syncOptions": {
|
||||
"initialSyncBehavior": "overwrite-destination",
|
||||
"prependPrefix": "INF_",
|
||||
"initialSyncBehavior": "overwrite-destination"
|
||||
},
|
||||
"destinationConfig": {
|
||||
"scope": "repository",
|
||||
"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,
|
||||
"lastRemovedAt": null,
|
||||
"syncOptions": {
|
||||
"initialSyncBehavior": "overwrite-destination",
|
||||
"prependPrefix": "INF_"
|
||||
"initialSyncBehavior": "overwrite-destination"
|
||||
},
|
||||
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"connection": {
|
||||
@@ -157,7 +153,7 @@ description: "Learn how to configure a GitHub Sync for Infisical."
|
||||
"destinationConfig": {
|
||||
"scope": "repository",
|
||||
"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 { 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 { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import {
|
||||
@@ -43,6 +43,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
||||
const { name: destinationName } = SECRET_SYNC_MAP[destination];
|
||||
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||
const [confirmOverwrite, setConfirmOverwrite] = useState(false);
|
||||
|
||||
const { syncOption } = useSecretSyncOption(destination);
|
||||
|
||||
@@ -93,7 +94,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
||||
setSelectedTabIndex((prev) => prev - 1);
|
||||
};
|
||||
|
||||
const { handleSubmit, trigger } = formMethods;
|
||||
const { handleSubmit, trigger, watch, control } = formMethods;
|
||||
|
||||
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
|
||||
|
||||
@@ -122,7 +123,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
||||
return isEnabled;
|
||||
};
|
||||
|
||||
const { control } = formMethods;
|
||||
const initialSyncBehavior = watch("syncOptions.initialSyncBehavior");
|
||||
|
||||
return (
|
||||
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}>
|
||||
@@ -195,10 +196,33 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</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">
|
||||
<Button onClick={handleNext} colorSchema="secondary">
|
||||
<Button
|
||||
isDisabled={
|
||||
isFinalStep &&
|
||||
initialSyncBehavior === SecretSyncInitialSyncBehavior.OverwriteDestination &&
|
||||
!confirmOverwrite
|
||||
}
|
||||
onClick={handleNext}
|
||||
colorSchema="secondary"
|
||||
>
|
||||
{isFinalStep ? "Create Sync" : "Next"}
|
||||
</Button>
|
||||
{}
|
||||
{selectedTabIndex > 0 && (
|
||||
<Button onClick={handlePrev} colorSchema="secondary">
|
||||
Back
|
||||
|
||||
@@ -35,12 +35,13 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) =>
|
||||
reValidateMode: "onChange"
|
||||
});
|
||||
|
||||
const onSubmit = async ({ environment, ...formData }: TSecretSyncForm) => {
|
||||
const onSubmit = async ({ environment, connection, ...formData }: TSecretSyncForm) => {
|
||||
try {
|
||||
const updatedSecretSync = await updateSecretSync.mutateAsync({
|
||||
syncId: secretSync.id,
|
||||
...formData,
|
||||
environment: environment?.slug
|
||||
environment: environment?.slug,
|
||||
connectionId: connection.id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Controller, useFormContext } from "react-hook-form";
|
||||
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
||||
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 { useSecretSyncOption } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
@@ -91,7 +91,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Controller
|
||||
{/* <Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
@@ -118,7 +118,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
)}
|
||||
control={control}
|
||||
name="syncOptions.appendSuffix"
|
||||
/>
|
||||
/> */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,7 +21,10 @@ export const SecretSyncReviewFields = () => {
|
||||
connection,
|
||||
environment,
|
||||
secretPath,
|
||||
syncOptions: { appendSuffix, prependPrefix, initialSyncBehavior },
|
||||
syncOptions: {
|
||||
// appendSuffix, prependPrefix,
|
||||
initialSyncBehavior
|
||||
},
|
||||
destination,
|
||||
isEnabled
|
||||
} = watch();
|
||||
@@ -72,8 +75,8 @@ export const SecretSyncReviewFields = () => {
|
||||
<SecretSyncLabel label="Initial Sync Behavior">
|
||||
{SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destinationName).name}
|
||||
</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Prepend Prefix">{prependPrefix}</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Append Suffix">{appendSuffix}</SecretSyncLabel>
|
||||
{/* <SecretSyncLabel label="Prepend Prefix">{prependPrefix}</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Append Suffix">{appendSuffix}</SecretSyncLabel> */}
|
||||
</div>
|
||||
</div>
|
||||
<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() }),
|
||||
secretPath: z.string().min(1, "Secret path required"),
|
||||
syncOptions: z.object({
|
||||
initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior),
|
||||
prependPrefix: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((str) => str.toUpperCase())
|
||||
.optional(),
|
||||
appendSuffix: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((str) => str.toUpperCase())
|
||||
.optional()
|
||||
initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior)
|
||||
// scott: removed temporarily for evaluation of template formatting
|
||||
// prependPrefix: z
|
||||
// .string()
|
||||
// .trim()
|
||||
// .transform((str) => str.toUpperCase())
|
||||
// .optional(),
|
||||
// appendSuffix: z
|
||||
// .string()
|
||||
// .trim()
|
||||
// .transform((str) => str.toUpperCase())
|
||||
// .optional()
|
||||
}),
|
||||
isEnabled: z.boolean()
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ export type TCreateSecretSyncDTO = DiscriminativePick<
|
||||
> & { environment: string; secretPath: string; projectId: string };
|
||||
|
||||
export type TUpdateSecretSyncDTO = Partial<
|
||||
Omit<TCreateSecretSyncDTO, "connectionId" | "destination" | "projectId">
|
||||
Omit<TCreateSecretSyncDTO, "destination" | "projectId">
|
||||
> & {
|
||||
destination: SecretSync;
|
||||
syncId: string;
|
||||
|
||||
@@ -26,8 +26,8 @@ export type TRootSecretSync = {
|
||||
lastRemoveMessage: string | null;
|
||||
syncOptions: {
|
||||
initialSyncBehavior: SecretSyncInitialSyncBehavior;
|
||||
prependPrefix?: string;
|
||||
appendSuffix?: string;
|
||||
// prependPrefix?: string;
|
||||
// appendSuffix?: string;
|
||||
};
|
||||
connection: {
|
||||
app: AppConnection;
|
||||
|
||||
@@ -19,7 +19,7 @@ export const SecretSyncsTab = () => {
|
||||
const { data: secretSyncs = [], isPending: isSecretSyncsPending } = useListSecretSyncs(
|
||||
currentWorkspace.id,
|
||||
{
|
||||
refetchInterval: 2000
|
||||
refetchInterval: 4000
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ const PageContent = () => {
|
||||
|
||||
const handleEditSource = () => handlePopUpOpen("editSync", SecretSyncEditFields.Source);
|
||||
|
||||
const handleEditOptions = () => handlePopUpOpen("editSync", SecretSyncEditFields.Options);
|
||||
// const handleEditOptions = () => handlePopUpOpen("editSync", SecretSyncEditFields.Options);
|
||||
|
||||
const handleEditDestination = () => handlePopUpOpen("editSync", SecretSyncEditFields.Destination);
|
||||
|
||||
@@ -109,7 +109,10 @@ const PageContent = () => {
|
||||
<div className="mr-4 flex w-72 flex-col gap-4">
|
||||
<SecretSyncDetailsSection secretSync={secretSync} onEditDetails={handleEditDetails} />
|
||||
<SecretSyncSourceSection secretSync={secretSync} onEditSource={handleEditSource} />
|
||||
<SecretSyncOptionsSection secretSync={secretSync} onEditOptions={handleEditOptions} />
|
||||
<SecretSyncOptionsSection
|
||||
secretSync={secretSync}
|
||||
// onEditOptions={handleEditOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-4">
|
||||
<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 { 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 { TSecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
type Props = {
|
||||
secretSync: TSecretSync;
|
||||
onEditOptions: VoidFunction;
|
||||
// onEditOptions: VoidFunction;
|
||||
};
|
||||
|
||||
export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) => {
|
||||
export const SecretSyncOptionsSection = ({
|
||||
secretSync
|
||||
// onEditOptions
|
||||
}: Props) => {
|
||||
const {
|
||||
destination,
|
||||
syncOptions: { appendSuffix, prependPrefix, initialSyncBehavior },
|
||||
lastSyncedAt
|
||||
syncOptions: {
|
||||
// appendSuffix,
|
||||
// prependPrefix,
|
||||
initialSyncBehavior
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
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 items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="font-semibold text-mineshaft-100">Sync Options</h3>
|
||||
<ProjectPermissionCan
|
||||
{/* <ProjectPermissionCan
|
||||
I={ProjectPermissionSecretSyncActions.Edit}
|
||||
a={ProjectPermissionSub.SecretSyncs}
|
||||
>
|
||||
@@ -41,17 +40,15 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</ProjectPermissionCan> */}
|
||||
</div>
|
||||
<div>
|
||||
<div className="space-y-3">
|
||||
{!lastSyncedAt && (
|
||||
<SecretSyncLabel label="Initial Sync Behavior">
|
||||
{SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destination).name}
|
||||
</SecretSyncLabel>
|
||||
)}
|
||||
<SecretSyncLabel label="Prefix">{prependPrefix}</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Suffix">{appendSuffix}</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Initial Sync Behavior">
|
||||
{SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destination).name}
|
||||
</SecretSyncLabel>
|
||||
{/* <SecretSyncLabel label="Prefix">{prependPrefix}</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Suffix">{appendSuffix}</SecretSyncLabel> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user