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

View File

@@ -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.`,

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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)
});
}

View File

@@ -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;

View File

@@ -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()

View File

@@ -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)

View File

@@ -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;
};

View File

@@ -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": {

View File

@@ -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"
}
}
}

View File

@@ -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

View File

@@ -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({

View File

@@ -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"
/>
/> */}
</>
);
};

View File

@@ -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">

View File

@@ -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()
});

View File

@@ -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;

View File

@@ -26,8 +26,8 @@ export type TRootSecretSync = {
lastRemoveMessage: string | null;
syncOptions: {
initialSyncBehavior: SecretSyncInitialSyncBehavior;
prependPrefix?: string;
appendSuffix?: string;
// prependPrefix?: string;
// appendSuffix?: string;
};
connection: {
app: AppConnection;

View File

@@ -19,7 +19,7 @@ export const SecretSyncsTab = () => {
const { data: secretSyncs = [], isPending: isSecretSyncsPending } = useListSecretSyncs(
currentWorkspace.id,
{
refetchInterval: 2000
refetchInterval: 4000
}
);

View File

@@ -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

View File

@@ -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>