From 0564d06923d4549f961857b6128bc971d060da4e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 19 Sep 2024 17:35:52 +0400 Subject: [PATCH 1/5] feat(integrations): email when integration sync fails --- backend/src/services/secret/secret-queue.ts | 31 +++++++++++++++++++ backend/src/services/smtp/smtp-service.ts | 3 +- .../integrationSyncFailed.handlebars | 24 ++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 backend/src/services/smtp/templates/integrationSyncFailed.handlebars diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 07cb0923d..50c77da2b 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -562,6 +562,30 @@ export const secretQueueFactory = ({ throw new Error("Secret path not found"); } + const sendIntegrationSyncFailedMail = async (integrationId: string, syncMessage: string | null) => { + const appCfg = getConfig(); + + // If smtp is not configured, we can return early without having to fetch the project members. + if (!appCfg.isSmtpConfigured) return; + + const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId); + const project = await projectDAL.findById(projectId); + + const filteredProjectMembers = + isManual && actorId ? projectMembers.filter((member) => member.userId === actorId) : projectMembers; + + await smtpService.sendMail({ + recipients: filteredProjectMembers.map((member) => member.user.email!), + template: SmtpTemplates.IntegrationSyncFailed, + subjectLine: `Integration Sync Failed`, + substitutions: { + syncMessage, + projectName: project.name, + integrationUrl: `${appCfg.SITE_URL}/integrations/details/${integrationId}` + } + }); + }; + // find all imports made with the given environment and secret path const linkSourceDto = { projectId, @@ -833,6 +857,11 @@ export const secretQueueFactory = ({ syncMessage: response?.syncMessage ?? "", isSynced: response?.isSynced ?? true }); + + // May be undefined, if it's undefined we assume the sync was successful, hence the strict equality type check. + if (response?.isSynced === false) { + await sendIntegrationSyncFailedMail(integration.id, response?.syncMessage); + } } catch (err) { logger.error( err, @@ -863,6 +892,8 @@ export const secretQueueFactory = ({ syncMessage: message, isSynced: false }); + + await sendIntegrationSyncFailedMail(integration.id, message); } } } finally { diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index d0e1e0774..823da4cca 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -33,7 +33,8 @@ export enum SmtpTemplates { SecretLeakIncident = "secretLeakIncident.handlebars", WorkspaceInvite = "workspaceInvitation.handlebars", ScimUserProvisioned = "scimUserProvisioned.handlebars", - PkiExpirationAlert = "pkiExpirationAlert.handlebars" + PkiExpirationAlert = "pkiExpirationAlert.handlebars", + IntegrationSyncFailed = "integrationSyncFailed.handlebars" } export enum SmtpHost { diff --git a/backend/src/services/smtp/templates/integrationSyncFailed.handlebars b/backend/src/services/smtp/templates/integrationSyncFailed.handlebars new file mode 100644 index 000000000..4f5c35014 --- /dev/null +++ b/backend/src/services/smtp/templates/integrationSyncFailed.handlebars @@ -0,0 +1,24 @@ + + + + + + Integration Sync Failed + + + +

Infisical

+

An integration in your project + {{projectName}} + failed to sync secrets.
+ + View integration details. + +

+ + {{#if syncMessage}} +

Reason: {{syncMessage}}

+ {{/if}} + + + \ No newline at end of file From c9f6207e32e0ba6a3ffc04bc7afa305fdcac0a3e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 19 Sep 2024 21:19:41 +0400 Subject: [PATCH 2/5] fix: bundle integration emails by secret path --- backend/src/services/secret/secret-queue.ts | 35 ++++++++++++++----- .../integrationSyncFailed.handlebars | 17 ++++++--- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 50c77da2b..1926be6da 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -1,7 +1,13 @@ /* eslint-disable no-await-in-loop */ import { AxiosError } from "axios"; -import { ProjectUpgradeStatus, ProjectVersion, TSecretSnapshotSecretsV2, TSecretVersionsV2 } from "@app/db/schemas"; +import { + ProjectMembershipRole, + ProjectUpgradeStatus, + ProjectVersion, + TSecretSnapshotSecretsV2, + TSecretVersionsV2 +} from "@app/db/schemas"; import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { Actor, EventType } from "@app/ee/services/audit-log/audit-log-types"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; @@ -562,7 +568,7 @@ export const secretQueueFactory = ({ throw new Error("Secret path not found"); } - const sendIntegrationSyncFailedMail = async (integrationId: string, syncMessage: string | null) => { + const sendIntegrationSyncFailedMail = async (integrations: { integrationId: string; syncMessage?: string }[]) => { const appCfg = getConfig(); // If smtp is not configured, we can return early without having to fetch the project members. @@ -571,17 +577,22 @@ export const secretQueueFactory = ({ const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId); const project = await projectDAL.findById(projectId); - const filteredProjectMembers = - isManual && actorId ? projectMembers.filter((member) => member.userId === actorId) : projectMembers; + // Only send emails to admins, and if its a manual trigger, only send it to the person who triggered it (if actor is admin as well) + const filteredProjectMembers = projectMembers + .filter((member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin)) + .filter((member) => (isManual && actorId ? member.userId === actorId : true)); await smtpService.sendMail({ recipients: filteredProjectMembers.map((member) => member.user.email!), template: SmtpTemplates.IntegrationSyncFailed, subjectLine: `Integration Sync Failed`, substitutions: { - syncMessage, + syncMessage: integrations[0]?.syncMessage, // We are only displaying the sync message if its a singular integration, so we can just grab the first one in the array. + secretPath, + environment: folder.environment.name, + count: integrations.length, projectName: project.name, - integrationUrl: `${appCfg.SITE_URL}/integrations/details/${integrationId}` + integrationUrl: `${appCfg.SITE_URL}/integrations/${project.id}` } }); }; @@ -687,6 +698,8 @@ export const secretQueueFactory = ({ ({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath) ); + const integrationsFailedToSync: { integrationId: string }[] = []; + if (!integrations.length) return; logger.info( `getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]` @@ -860,7 +873,9 @@ export const secretQueueFactory = ({ // May be undefined, if it's undefined we assume the sync was successful, hence the strict equality type check. if (response?.isSynced === false) { - await sendIntegrationSyncFailedMail(integration.id, response?.syncMessage); + integrationsFailedToSync.push({ + integrationId: integration.id + }); } } catch (err) { logger.error( @@ -893,11 +908,15 @@ export const secretQueueFactory = ({ isSynced: false }); - await sendIntegrationSyncFailedMail(integration.id, message); + integrationsFailedToSync.push({ + integrationId: integration.id + }); } } } finally { await lock.release(); + + await sendIntegrationSyncFailedMail(integrationsFailedToSync); } await keyStore.setItemWithExpiry( diff --git a/backend/src/services/smtp/templates/integrationSyncFailed.handlebars b/backend/src/services/smtp/templates/integrationSyncFailed.handlebars index 4f5c35014..5c5d76693 100644 --- a/backend/src/services/smtp/templates/integrationSyncFailed.handlebars +++ b/backend/src/services/smtp/templates/integrationSyncFailed.handlebars @@ -8,13 +8,20 @@

Infisical

-

An integration in your project - {{projectName}} - failed to sync secrets.
+ +

+

{{count}} integration(s) failed to sync.

- View integration details. + View your project integrations. -

+
+ +
+
+

Project: {{projectName}}

+

Environment: {{environment}}

+

Secret Path: {{secretPath}}

+
{{#if syncMessage}}

Reason: {{syncMessage}}

From 998bbe92f788e0a1e7fea13b044e0deb30835282 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 20 Sep 2024 00:07:09 +0400 Subject: [PATCH 3/5] feat: failed integration sync emails debouncer --- backend/src/keystore/keystore.ts | 7 +- backend/src/queue/queue-service.ts | 48 +- backend/src/services/secret/secret-queue.ts | 704 +++++++++++--------- backend/src/services/secret/secret-types.ts | 24 + 4 files changed, 447 insertions(+), 336 deletions(-) diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index 3b19012e7..bff0ad885 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -22,12 +22,15 @@ export const KeyStorePrefixes = { `sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const, IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) => `identity-access-token-status:${identityAccessTokenId}`, - ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}` + ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`, + SendFailedIntegrationSyncEmails: (projectId: string, secretPath: string, environmentSlug: string) => + `send-failed-integration-sync-emails-${projectId}-${secretPath}-${environmentSlug}` as const }; export const KeyStoreTtls = { SetSyncSecretIntegrationLastRunTimestampInSeconds: 10, - AccessTokenStatusUpdateInSeconds: 120 + AccessTokenStatusUpdateInSeconds: 120, + SendFailedIntegrationSyncEmailsInSeconds: 60 }; type TWaitTillReady = { diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index 68fcb0db2..1a5560d3a 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -7,7 +7,7 @@ import { TScanFullRepoEventPayload, TScanPushEventPayload } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types"; -import { TSyncSecretsDTO } from "@app/services/secret/secret-types"; +import { TIntegrationSyncPayload, TSyncSecretsDTO } from "@app/services/secret/secret-types"; export enum QueueName { SecretRotation = "secret-rotation", @@ -42,6 +42,7 @@ export enum QueueJobs { SecWebhook = "secret-webhook-trigger", TelemetryInstanceStats = "telemetry-self-hosted-stats", IntegrationSync = "secret-integration-pull", + SendFailedIntegrationSyncEmails = "send-failed-integration-sync-emails", SecretScan = "secret-scan", UpgradeProjectToGhost = "upgrade-project-to-ghost-job", DynamicSecretRevocation = "dynamic-secret-revocation", @@ -88,18 +89,30 @@ export type TQueueJobTypes = { name: QueueJobs.SecWebhook; payload: { projectId: string; environment: string; secretPath: string; depth?: number }; }; - [QueueName.IntegrationSync]: { - name: QueueJobs.IntegrationSync; - payload: { - isManual?: boolean; - actorId?: string; - projectId: string; - environment: string; - secretPath: string; - depth?: number; - deDupeQueue?: Record; - }; - }; + + [QueueName.AccessTokenStatusUpdate]: + | { + name: QueueJobs.IdentityAccessTokenStatusUpdate; + payload: { identityAccessTokenId: string; numberOfUses: number }; + } + | { + name: QueueJobs.ServiceTokenStatusUpdate; + payload: { serviceTokenId: string }; + }; + + [QueueName.IntegrationSync]: + | { + name: QueueJobs.IntegrationSync; + payload: TIntegrationSyncPayload; + } + | { + name: QueueJobs.SendFailedIntegrationSyncEmails; + payload: { + projectId: string; + environmentSlug: string; + secretPath: string; + }; + }; [QueueName.SecretFullRepoScan]: { name: QueueJobs.SecretScan; payload: TScanFullRepoEventPayload; @@ -153,15 +166,6 @@ export type TQueueJobTypes = { name: QueueJobs.ProjectV3Migration; payload: { projectId: string }; }; - [QueueName.AccessTokenStatusUpdate]: - | { - name: QueueJobs.IdentityAccessTokenStatusUpdate; - payload: { identityAccessTokenId: string; numberOfUses: number }; - } - | { - name: QueueJobs.ServiceTokenStatusUpdate; - payload: { serviceTokenId: string }; - }; }; export type TQueueServiceFactory = ReturnType; diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 1926be6da..e858a08a4 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -17,7 +17,7 @@ import { TSnapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/sn import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; -import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; +import { applyJitter, daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { BadRequestError } from "@app/lib/errors"; import { getTimeDifferenceInSeconds, groupBy, isSamePath, unique } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; @@ -55,8 +55,11 @@ import { fnTriggerWebhook } from "../webhook/webhook-fns"; import { TSecretDALFactory } from "./secret-dal"; import { interpolateSecrets } from "./secret-fns"; import { + FailedIntegrationSyncEmailsPayloadSchema, TCreateSecretReminderDTO, + TFailedIntegrationSyncEmailsPayload, THandleReminderDTO, + TIntegrationSyncPayload, TRemoveSecretReminderDTO, TSyncSecretsDTO } from "./secret-types"; @@ -515,6 +518,38 @@ export const secretQueueFactory = ({ ); }; + const sendFailedIntegrationSyncEmails = async (payload: TFailedIntegrationSyncEmailsPayload) => { + const key = KeyStorePrefixes.SendFailedIntegrationSyncEmails( + payload.projectId, + payload.secretPath, + payload.environmentSlug + ); + + await keyStore.setItemWithExpiry( + key, + KeyStoreTtls.SendFailedIntegrationSyncEmailsInSeconds, + JSON.stringify(payload) + ); + await queueService.queue( + QueueName.IntegrationSync, + QueueJobs.SendFailedIntegrationSyncEmails, + { + secretPath: payload.secretPath, + projectId: payload.projectId, + environmentSlug: payload.environmentSlug + }, + { + delay: applyJitter( + secondsToMillis(KeyStoreTtls.SendFailedIntegrationSyncEmailsInSeconds / 2), + secondsToMillis(10) + ), + jobId: key, + removeOnFail: true, + removeOnComplete: true + } + ); + }; + queueService.start(QueueName.SecretSync, async (job) => { const { _deDupeQueue: deDupeQueue, @@ -560,371 +595,416 @@ export const secretQueueFactory = ({ }); queueService.start(QueueName.IntegrationSync, async (job) => { - const { environment, actorId, isManual, projectId, secretPath, depth = 1, deDupeQueue = {} } = job.data; - if (depth > MAX_SYNC_SECRET_DEPTH) return; - - const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); - if (!folder) { - throw new Error("Secret path not found"); - } - - const sendIntegrationSyncFailedMail = async (integrations: { integrationId: string; syncMessage?: string }[]) => { + if (job.name === QueueJobs.SendFailedIntegrationSyncEmails) { const appCfg = getConfig(); - // If smtp is not configured, we can return early without having to fetch the project members. + // If smtp is not configured, we can return early if (!appCfg.isSmtpConfigured) return; - const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId); - const project = await projectDAL.findById(projectId); + const jobPayload = job.data as Pick< + TFailedIntegrationSyncEmailsPayload, + "projectId" | "secretPath" | "environmentSlug" + >; + + const failedIntegrationsDetails = await keyStore.getItem( + KeyStorePrefixes.SendFailedIntegrationSyncEmails( + jobPayload.projectId, + jobPayload.secretPath, + jobPayload.environmentSlug + ) + ); + + if (!failedIntegrationsDetails) return; + + const failedSyncKeyStore = FailedIntegrationSyncEmailsPayloadSchema.parse(JSON.parse(failedIntegrationsDetails)); + + const projectMembers = await projectMembershipDAL.findAllProjectMembers(failedSyncKeyStore.projectId); + const project = await projectDAL.findById(failedSyncKeyStore.projectId); // Only send emails to admins, and if its a manual trigger, only send it to the person who triggered it (if actor is admin as well) const filteredProjectMembers = projectMembers .filter((member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin)) - .filter((member) => (isManual && actorId ? member.userId === actorId : true)); + .filter((member) => + failedSyncKeyStore.manuallyTriggeredByUserId + ? member.userId === failedSyncKeyStore.manuallyTriggeredByUserId + : true + ); await smtpService.sendMail({ recipients: filteredProjectMembers.map((member) => member.user.email!), template: SmtpTemplates.IntegrationSyncFailed, subjectLine: `Integration Sync Failed`, substitutions: { - syncMessage: integrations[0]?.syncMessage, // We are only displaying the sync message if its a singular integration, so we can just grab the first one in the array. - secretPath, - environment: folder.environment.name, - count: integrations.length, + syncMessage: failedSyncKeyStore.count === 1 ? failedSyncKeyStore.syncMessage : undefined, // We are only displaying the sync message if its a singular integration, so we can just grab the first one in the array. + secretPath: failedSyncKeyStore.secretPath, + environment: failedSyncKeyStore.environmentName, + count: failedSyncKeyStore.count, projectName: project.name, integrationUrl: `${appCfg.SITE_URL}/integrations/${project.id}` } }); - }; - - // find all imports made with the given environment and secret path - const linkSourceDto = { - projectId, - importEnv: folder.environment.id, - importPath: secretPath, - isReplication: false - }; - const imports = await secretImportDAL.find(linkSourceDto); - - if (imports.length) { - // keep calling sync secret for all the imports made - const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId); - const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds); - const foldersGroupedById = groupBy(importedFolders.filter(Boolean), (i) => i?.id as string); - logger.info( - `getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]` - ); - await Promise.all( - imports - .filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0]?.path as string)) - // filter out already synced ones - .filter( - ({ folderId }) => - !deDupeQueue[ - uniqueSecretQueueKey( - foldersGroupedById[folderId][0]?.environmentSlug as string, - foldersGroupedById[folderId][0]?.path as string - ) - ] - ) - .map(({ folderId }) => - syncSecrets({ - projectId, - secretPath: foldersGroupedById[folderId][0]?.path as string, - environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string, - _deDupeQueue: deDupeQueue, - _depth: depth + 1, - excludeReplication: true - }) - ) - ); } - const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId); - const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - let referencedFolderIds; - if (shouldUseSecretV2Bridge) { - const secretReferences = await secretV2BridgeDAL.findReferencedSecretReferences( + + if (job.name === QueueJobs.IntegrationSync) { + const { + environment, + actorId, + isManual, projectId, - folder.environment.slug, - secretPath - ); - referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId); - } else { - const secretReferences = await secretDAL.findReferencedSecretReferences( - projectId, - folder.environment.slug, - secretPath - ); - referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId); - } - if (referencedFolderIds.length) { - const referencedFolders = await folderDAL.findSecretPathByFolderIds(projectId, referencedFolderIds); - const referencedFoldersGroupedById = groupBy(referencedFolders.filter(Boolean), (i) => i?.id as string); - logger.info( - `getIntegrationSecrets: Syncing secret due to reference change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]` - ); - await Promise.all( - referencedFolderIds - .filter((folderId) => Boolean(referencedFoldersGroupedById[folderId][0]?.path)) - // filter out already synced ones - .filter( - (folderId) => - !deDupeQueue[ - uniqueSecretQueueKey( - referencedFoldersGroupedById[folderId][0]?.environmentSlug as string, - referencedFoldersGroupedById[folderId][0]?.path as string - ) - ] - ) - .map((folderId) => - syncSecrets({ - projectId, - secretPath: referencedFoldersGroupedById[folderId][0]?.path as string, - environmentSlug: referencedFoldersGroupedById[folderId][0]?.environmentSlug as string, - _deDupeQueue: deDupeQueue, - _depth: depth + 1, - excludeReplication: true - }) - ) - ); - } + secretPath, + depth = 1, + deDupeQueue = {} + } = job.data as TIntegrationSyncPayload; + if (depth > MAX_SYNC_SECRET_DEPTH) return; - const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment - const toBeSyncedIntegrations = integrations.filter( - // note: sync only the integrations sourced from secretPath - ({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath) - ); - - const integrationsFailedToSync: { integrationId: string }[] = []; - - if (!integrations.length) return; - logger.info( - `getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]` - ); - - const lock = await keyStore.acquireLock( - [KeyStorePrefixes.SyncSecretIntegrationLock(projectId, environment, secretPath)], - 10000, - { - retryCount: 3, - retryDelay: 2000 + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + if (!folder) { + throw new Error("Secret path not found"); } - ); - const lockAcquiredTime = new Date(); - const lastRunSyncIntegrationTimestamp = await keyStore.getItem( - KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath) - ); + // find all imports made with the given environment and secret path + const linkSourceDto = { + projectId, + importEnv: folder.environment.id, + importPath: secretPath, + isReplication: false + }; + const imports = await secretImportDAL.find(linkSourceDto); - // check whether the integration should wait or not - if (lastRunSyncIntegrationTimestamp) { - const INTEGRATION_INTERVAL = 2000; - const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp); - if (isStaleSyncIntegration) { + if (imports.length) { + // keep calling sync secret for all the imports made + const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId); + const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds); + const foldersGroupedById = groupBy(importedFolders.filter(Boolean), (i) => i?.id as string); logger.info( - `getIntegrationSecrets: secret integration sync stale [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]` + `getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]` + ); + await Promise.all( + imports + .filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0]?.path as string)) + // filter out already synced ones + .filter( + ({ folderId }) => + !deDupeQueue[ + uniqueSecretQueueKey( + foldersGroupedById[folderId][0]?.environmentSlug as string, + foldersGroupedById[folderId][0]?.path as string + ) + ] + ) + .map(({ folderId }) => + syncSecrets({ + projectId, + secretPath: foldersGroupedById[folderId][0]?.path as string, + environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string, + _deDupeQueue: deDupeQueue, + _depth: depth + 1, + excludeReplication: true + }) + ) + ); + } + const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + let referencedFolderIds; + if (shouldUseSecretV2Bridge) { + const secretReferences = await secretV2BridgeDAL.findReferencedSecretReferences( + projectId, + folder.environment.slug, + secretPath + ); + referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId); + } else { + const secretReferences = await secretDAL.findReferencedSecretReferences( + projectId, + folder.environment.slug, + secretPath + ); + referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId); + } + if (referencedFolderIds.length) { + const referencedFolders = await folderDAL.findSecretPathByFolderIds(projectId, referencedFolderIds); + const referencedFoldersGroupedById = groupBy(referencedFolders.filter(Boolean), (i) => i?.id as string); + logger.info( + `getIntegrationSecrets: Syncing secret due to reference change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]` + ); + await Promise.all( + referencedFolderIds + .filter((folderId) => Boolean(referencedFoldersGroupedById[folderId][0]?.path)) + // filter out already synced ones + .filter( + (folderId) => + !deDupeQueue[ + uniqueSecretQueueKey( + referencedFoldersGroupedById[folderId][0]?.environmentSlug as string, + referencedFoldersGroupedById[folderId][0]?.path as string + ) + ] + ) + .map((folderId) => + syncSecrets({ + projectId, + secretPath: referencedFoldersGroupedById[folderId][0]?.path as string, + environmentSlug: referencedFoldersGroupedById[folderId][0]?.environmentSlug as string, + _deDupeQueue: deDupeQueue, + _depth: depth + 1, + excludeReplication: true + }) + ) ); - return; } - const timeDifferenceWithLastIntegration = getTimeDifferenceInSeconds( - lockAcquiredTime.toISOString(), - lastRunSyncIntegrationTimestamp + const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment + const toBeSyncedIntegrations = integrations.filter( + // note: sync only the integrations sourced from secretPath + ({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath) ); - if (timeDifferenceWithLastIntegration < INTEGRATION_INTERVAL && timeDifferenceWithLastIntegration > 0) - await new Promise((resolve) => { - setTimeout(resolve, 2000 - timeDifferenceWithLastIntegration * 1000); - }); - } - const generateActor = async (): Promise => { - if (isManual && actorId) { - const user = await userDAL.findById(actorId); + const integrationsFailedToSync: { integrationId: string; syncMessage?: string }[] = []; - if (!user) { - throw new Error("User not found"); + if (!integrations.length) return; + logger.info( + `getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]` + ); + + const lock = await keyStore.acquireLock( + [KeyStorePrefixes.SyncSecretIntegrationLock(projectId, environment, secretPath)], + 10000, + { + retryCount: 3, + retryDelay: 2000 + } + ); + const lockAcquiredTime = new Date(); + + const lastRunSyncIntegrationTimestamp = await keyStore.getItem( + KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath) + ); + + // check whether the integration should wait or not + if (lastRunSyncIntegrationTimestamp) { + const INTEGRATION_INTERVAL = 2000; + const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp); + if (isStaleSyncIntegration) { + logger.info( + `getIntegrationSecrets: secret integration sync stale [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]` + ); + return; + } + + const timeDifferenceWithLastIntegration = getTimeDifferenceInSeconds( + lockAcquiredTime.toISOString(), + lastRunSyncIntegrationTimestamp + ); + if (timeDifferenceWithLastIntegration < INTEGRATION_INTERVAL && timeDifferenceWithLastIntegration > 0) + await new Promise((resolve) => { + setTimeout(resolve, 2000 - timeDifferenceWithLastIntegration * 1000); + }); + } + + const generateActor = async (): Promise => { + if (isManual && actorId) { + const user = await userDAL.findById(actorId); + + if (!user) { + throw new Error("User not found"); + } + + return { + type: ActorType.USER, + metadata: { + email: user.email, + username: user.username, + userId: user.id + } + }; } return { - type: ActorType.USER, - metadata: { - email: user.email, - username: user.username, - userId: user.id - } + type: ActorType.PLATFORM, + metadata: {} }; - } - - return { - type: ActorType.PLATFORM, - metadata: {} }; - }; - // akhilmhdh: this try catch is for lock release - try { - const secrets = shouldUseSecretV2Bridge - ? await getIntegrationSecretsV2({ - environment, - projectId, - folderId: folder.id, - depth: 1, - secretPath, - decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "") - }) - : await getIntegrationSecrets({ - environment, - projectId, - folderId: folder.id, - key: botKey as string, - depth: 1, - secretPath - }); + // akhilmhdh: this try catch is for lock release + try { + const secrets = shouldUseSecretV2Bridge + ? await getIntegrationSecretsV2({ + environment, + projectId, + folderId: folder.id, + depth: 1, + secretPath, + decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "") + }) + : await getIntegrationSecrets({ + environment, + projectId, + folderId: folder.id, + key: botKey as string, + depth: 1, + secretPath + }); - for (const integration of toBeSyncedIntegrations) { - const integrationAuth = { - ...integration.integrationAuth, - createdAt: new Date(), - updatedAt: new Date(), - projectId: integration.projectId - }; + for (const integration of toBeSyncedIntegrations) { + const integrationAuth = { + ...integration.integrationAuth, + createdAt: new Date(), + updatedAt: new Date(), + projectId: integration.projectId + }; - const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken( - integrationAuth, - shouldUseSecretV2Bridge, - botKey - ); - let awsAssumeRoleArn = null; - if (shouldUseSecretV2Bridge) { - if (integrationAuth.encryptedAwsAssumeIamRoleArn) { - awsAssumeRoleArn = secretManagerDecryptor({ - cipherTextBlob: Buffer.from(integrationAuth.encryptedAwsAssumeIamRoleArn) - }).toString(); - } - } else if ( - integrationAuth.awsAssumeIamRoleArnTag && - integrationAuth.awsAssumeIamRoleArnIV && - integrationAuth.awsAssumeIamRoleArnCipherText - ) { - awsAssumeRoleArn = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: integrationAuth.awsAssumeIamRoleArnCipherText, - iv: integrationAuth.awsAssumeIamRoleArnIV, - tag: integrationAuth.awsAssumeIamRoleArnTag, - key: botKey as string - }); - } - - const suffixedSecrets: typeof secrets = {}; - const metadata = integration.metadata as Record; - if (metadata) { - Object.keys(secrets).forEach((key) => { - const prefix = metadata?.secretPrefix || ""; - const suffix = metadata?.secretSuffix || ""; - const newKey = prefix + key + suffix; - suffixedSecrets[newKey] = secrets[key]; - }); - } - - // akhilmhdh: this try catch is for catching integration error and saving it in db - try { - // akhilmhdh: this needs to changed later to be more easier to use - // at present this is not at all extendable like to add a new parameter for just one integration need to modify multiple places - const response = await syncIntegrationSecrets({ - createManySecretsRawFn, - updateManySecretsRawFn, - integrationDAL, - integration, + const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken( integrationAuth, - secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets, - accessId: accessId as string, - awsAssumeRoleArn, - accessToken, - projectId, - appendices: { - prefix: metadata?.secretPrefix || "", - suffix: metadata?.secretSuffix || "" + shouldUseSecretV2Bridge, + botKey + ); + let awsAssumeRoleArn = null; + if (shouldUseSecretV2Bridge) { + if (integrationAuth.encryptedAwsAssumeIamRoleArn) { + awsAssumeRoleArn = secretManagerDecryptor({ + cipherTextBlob: Buffer.from(integrationAuth.encryptedAwsAssumeIamRoleArn) + }).toString(); } - }); - - await auditLogService.createAuditLog({ - projectId, - actor: await generateActor(), - event: { - type: EventType.INTEGRATION_SYNCED, - metadata: { - integrationId: integration.id, - isSynced: response?.isSynced ?? true, - lastSyncJobId: job?.id ?? "", - lastUsed: new Date(), - syncMessage: response?.syncMessage ?? "" - } - } - }); - - await integrationDAL.updateById(integration.id, { - lastSyncJobId: job.id, - lastUsed: new Date(), - syncMessage: response?.syncMessage ?? "", - isSynced: response?.isSynced ?? true - }); - - // May be undefined, if it's undefined we assume the sync was successful, hence the strict equality type check. - if (response?.isSynced === false) { - integrationsFailedToSync.push({ - integrationId: integration.id + } else if ( + integrationAuth.awsAssumeIamRoleArnTag && + integrationAuth.awsAssumeIamRoleArnIV && + integrationAuth.awsAssumeIamRoleArnCipherText + ) { + awsAssumeRoleArn = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: integrationAuth.awsAssumeIamRoleArnCipherText, + iv: integrationAuth.awsAssumeIamRoleArnIV, + tag: integrationAuth.awsAssumeIamRoleArnTag, + key: botKey as string }); } - } catch (err) { - logger.error( - err, - `Secret integration sync error [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}]` - ); - const message = - (err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) || - "Unknown error occurred."; + const suffixedSecrets: typeof secrets = {}; + const metadata = integration.metadata as Record; + if (metadata) { + Object.keys(secrets).forEach((key) => { + const prefix = metadata?.secretPrefix || ""; + const suffix = metadata?.secretSuffix || ""; + const newKey = prefix + key + suffix; + suffixedSecrets[newKey] = secrets[key]; + }); + } - await auditLogService.createAuditLog({ - projectId, - actor: await generateActor(), - event: { - type: EventType.INTEGRATION_SYNCED, - metadata: { - integrationId: integration.id, - isSynced: false, - lastSyncJobId: job?.id ?? "", - lastUsed: new Date(), - syncMessage: message + // akhilmhdh: this try catch is for catching integration error and saving it in db + try { + // akhilmhdh: this needs to changed later to be more easier to use + // at present this is not at all extendable like to add a new parameter for just one integration need to modify multiple places + const response = await syncIntegrationSecrets({ + createManySecretsRawFn, + updateManySecretsRawFn, + integrationDAL, + integration, + integrationAuth, + secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets, + accessId: accessId as string, + awsAssumeRoleArn, + accessToken, + projectId, + appendices: { + prefix: metadata?.secretPrefix || "", + suffix: metadata?.secretSuffix || "" } + }); + + await auditLogService.createAuditLog({ + projectId, + actor: await generateActor(), + event: { + type: EventType.INTEGRATION_SYNCED, + metadata: { + integrationId: integration.id, + isSynced: response?.isSynced ?? true, + lastSyncJobId: job?.id ?? "", + lastUsed: new Date(), + syncMessage: response?.syncMessage ?? "" + } + } + }); + + await integrationDAL.updateById(integration.id, { + lastSyncJobId: job.id, + lastUsed: new Date(), + syncMessage: response?.syncMessage ?? "", + isSynced: response?.isSynced ?? true + }); + + // May be undefined, if it's undefined we assume the sync was successful, hence the strict equality type check. + if (response?.isSynced === false) { + integrationsFailedToSync.push({ + integrationId: integration.id, + syncMessage: response.syncMessage + }); } - }); + } catch (err) { + logger.error( + err, + `Secret integration sync error [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}]` + ); - await integrationDAL.updateById(integration.id, { - lastSyncJobId: job.id, - syncMessage: message, - isSynced: false - }); + const message = + (err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) || + "Unknown error occurred."; - integrationsFailedToSync.push({ - integrationId: integration.id + await auditLogService.createAuditLog({ + projectId, + actor: await generateActor(), + event: { + type: EventType.INTEGRATION_SYNCED, + metadata: { + integrationId: integration.id, + isSynced: false, + lastSyncJobId: job?.id ?? "", + lastUsed: new Date(), + syncMessage: message + } + } + }); + + await integrationDAL.updateById(integration.id, { + lastSyncJobId: job.id, + syncMessage: message, + isSynced: false + }); + + integrationsFailedToSync.push({ + integrationId: integration.id, + syncMessage: message + }); + } + } + } finally { + await lock.release(); + if (integrationsFailedToSync.length) { + await sendFailedIntegrationSyncEmails({ + count: integrationsFailedToSync.length, + environmentName: folder.environment.name, + environmentSlug: environment, + ...(isManual && + actorId && { + manuallyTriggeredByUserId: actorId + }), + projectId, + secretPath, + syncMessage: integrationsFailedToSync[0].syncMessage }); } } - } finally { - await lock.release(); - await sendIntegrationSyncFailedMail(integrationsFailedToSync); + await keyStore.setItemWithExpiry( + KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath), + KeyStoreTtls.SetSyncSecretIntegrationLastRunTimestampInSeconds, + lockAcquiredTime.toISOString() + ); + logger.info("Secret integration sync ended: %s", job.id); } - - await keyStore.setItemWithExpiry( - KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath), - KeyStoreTtls.SetSyncSecretIntegrationLastRunTimestampInSeconds, - lockAcquiredTime.toISOString() - ); - logger.info("Secret integration sync ended: %s", job.id); }); queueService.start(QueueName.SecretReminder, async ({ data }) => { diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 1686ee488..b5281af68 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -1,4 +1,5 @@ import { Knex } from "knex"; +import { z } from "zod"; import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; @@ -21,6 +22,29 @@ type TPartialSecret = Pick; +export const FailedIntegrationSyncEmailsPayloadSchema = z.object({ + projectId: z.string(), + secretPath: z.string(), + environmentName: z.string(), + environmentSlug: z.string(), + + count: z.number(), + syncMessage: z.string().optional(), + manuallyTriggeredByUserId: z.string().optional() +}); + +export type TFailedIntegrationSyncEmailsPayload = z.infer; + +export type TIntegrationSyncPayload = { + isManual?: boolean; + actorId?: string; + projectId: string; + environment: string; + secretPath: string; + depth?: number; + deDupeQueue?: Record; +}; + export type TCreateSecretDTO = { secretName: string; path: string; From 4c4d525655d0eb6da814b2d4a8c59d0461180239 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 20 Sep 2024 22:20:32 +0400 Subject: [PATCH 4/5] fix: moved away from keystore since its not needed --- backend/src/queue/queue-service.ts | 12 ++-- backend/src/services/secret/secret-queue.ts | 76 +++++---------------- 2 files changed, 24 insertions(+), 64 deletions(-) diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index 1a5560d3a..0606f9dba 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -7,7 +7,11 @@ import { TScanFullRepoEventPayload, TScanPushEventPayload } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types"; -import { TIntegrationSyncPayload, TSyncSecretsDTO } from "@app/services/secret/secret-types"; +import { + TFailedIntegrationSyncEmailsPayload, + TIntegrationSyncPayload, + TSyncSecretsDTO +} from "@app/services/secret/secret-types"; export enum QueueName { SecretRotation = "secret-rotation", @@ -107,11 +111,7 @@ export type TQueueJobTypes = { } | { name: QueueJobs.SendFailedIntegrationSyncEmails; - payload: { - projectId: string; - environmentSlug: string; - secretPath: string; - }; + payload: TFailedIntegrationSyncEmailsPayload; }; [QueueName.SecretFullRepoScan]: { name: QueueJobs.SecretScan; diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index e858a08a4..4005c8f7a 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -17,7 +17,7 @@ import { TSnapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/sn import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; -import { applyJitter, daysToMillisecond, secondsToMillis } from "@app/lib/dates"; +import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { BadRequestError } from "@app/lib/errors"; import { getTimeDifferenceInSeconds, groupBy, isSamePath, unique } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; @@ -55,7 +55,6 @@ import { fnTriggerWebhook } from "../webhook/webhook-fns"; import { TSecretDALFactory } from "./secret-dal"; import { interpolateSecrets } from "./secret-fns"; import { - FailedIntegrationSyncEmailsPayloadSchema, TCreateSecretReminderDTO, TFailedIntegrationSyncEmailsPayload, THandleReminderDTO, @@ -519,35 +518,16 @@ export const secretQueueFactory = ({ }; const sendFailedIntegrationSyncEmails = async (payload: TFailedIntegrationSyncEmailsPayload) => { - const key = KeyStorePrefixes.SendFailedIntegrationSyncEmails( - payload.projectId, - payload.secretPath, - payload.environmentSlug - ); + const appCfg = getConfig(); + if (!appCfg.isSmtpConfigured) return; - await keyStore.setItemWithExpiry( - key, - KeyStoreTtls.SendFailedIntegrationSyncEmailsInSeconds, - JSON.stringify(payload) - ); - await queueService.queue( - QueueName.IntegrationSync, - QueueJobs.SendFailedIntegrationSyncEmails, - { - secretPath: payload.secretPath, - projectId: payload.projectId, - environmentSlug: payload.environmentSlug - }, - { - delay: applyJitter( - secondsToMillis(KeyStoreTtls.SendFailedIntegrationSyncEmailsInSeconds / 2), - secondsToMillis(10) - ), - jobId: key, - removeOnFail: true, - removeOnComplete: true - } - ); + await queueService.queue(QueueName.IntegrationSync, QueueJobs.SendFailedIntegrationSyncEmails, payload, { + jobId: `send-failed-integration-sync-emails-${payload.projectId}-${payload.secretPath}-${payload.environmentSlug}`, + delay: 1_000 * 60, // 1 minute + + removeOnFail: true, + removeOnComplete: true + }); }; queueService.start(QueueName.SecretSync, async (job) => { @@ -598,36 +578,16 @@ export const secretQueueFactory = ({ if (job.name === QueueJobs.SendFailedIntegrationSyncEmails) { const appCfg = getConfig(); - // If smtp is not configured, we can return early - if (!appCfg.isSmtpConfigured) return; + const jobPayload = job.data as TFailedIntegrationSyncEmailsPayload; - const jobPayload = job.data as Pick< - TFailedIntegrationSyncEmailsPayload, - "projectId" | "secretPath" | "environmentSlug" - >; - - const failedIntegrationsDetails = await keyStore.getItem( - KeyStorePrefixes.SendFailedIntegrationSyncEmails( - jobPayload.projectId, - jobPayload.secretPath, - jobPayload.environmentSlug - ) - ); - - if (!failedIntegrationsDetails) return; - - const failedSyncKeyStore = FailedIntegrationSyncEmailsPayloadSchema.parse(JSON.parse(failedIntegrationsDetails)); - - const projectMembers = await projectMembershipDAL.findAllProjectMembers(failedSyncKeyStore.projectId); - const project = await projectDAL.findById(failedSyncKeyStore.projectId); + const projectMembers = await projectMembershipDAL.findAllProjectMembers(jobPayload.projectId); + const project = await projectDAL.findById(jobPayload.projectId); // Only send emails to admins, and if its a manual trigger, only send it to the person who triggered it (if actor is admin as well) const filteredProjectMembers = projectMembers .filter((member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin)) .filter((member) => - failedSyncKeyStore.manuallyTriggeredByUserId - ? member.userId === failedSyncKeyStore.manuallyTriggeredByUserId - : true + jobPayload.manuallyTriggeredByUserId ? member.userId === jobPayload.manuallyTriggeredByUserId : true ); await smtpService.sendMail({ @@ -635,10 +595,10 @@ export const secretQueueFactory = ({ template: SmtpTemplates.IntegrationSyncFailed, subjectLine: `Integration Sync Failed`, substitutions: { - syncMessage: failedSyncKeyStore.count === 1 ? failedSyncKeyStore.syncMessage : undefined, // We are only displaying the sync message if its a singular integration, so we can just grab the first one in the array. - secretPath: failedSyncKeyStore.secretPath, - environment: failedSyncKeyStore.environmentName, - count: failedSyncKeyStore.count, + syncMessage: jobPayload.count === 1 ? jobPayload.syncMessage : undefined, // We are only displaying the sync message if its a singular integration, so we can just grab the first one in the array. + secretPath: jobPayload.secretPath, + environment: jobPayload.environmentName, + count: jobPayload.count, projectName: project.name, integrationUrl: `${appCfg.SITE_URL}/integrations/${project.id}` } From 3fc60bf59678bc8bca98f208b27d2e24f9395629 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 20 Sep 2024 22:29:44 +0400 Subject: [PATCH 5/5] Update keystore.ts --- backend/src/keystore/keystore.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index bff0ad885..3b19012e7 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -22,15 +22,12 @@ export const KeyStorePrefixes = { `sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const, IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) => `identity-access-token-status:${identityAccessTokenId}`, - ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`, - SendFailedIntegrationSyncEmails: (projectId: string, secretPath: string, environmentSlug: string) => - `send-failed-integration-sync-emails-${projectId}-${secretPath}-${environmentSlug}` as const + ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}` }; export const KeyStoreTtls = { SetSyncSecretIntegrationLastRunTimestampInSeconds: 10, - AccessTokenStatusUpdateInSeconds: 120, - SendFailedIntegrationSyncEmailsInSeconds: 60 + AccessTokenStatusUpdateInSeconds: 120 }; type TWaitTillReady = {