From 80352acc8a6c2c6d6e7d886810d017455a391b3d Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Mon, 14 Apr 2025 18:31:06 -0300 Subject: [PATCH 1/6] Add notification on Service Token expiration --- ...701_add-notification-flag-service-token.ts | 27 +++++++++++++++ backend/src/db/schemas/service-tokens.ts | 3 +- backend/src/server/routes/index.ts | 6 ++-- .../resource-cleanup-queue.ts | 6 +++- .../service-token/service-token-dal.ts | 26 +++++++++++++- .../service-token/service-token-service.ts | 34 +++++++++++++++++-- backend/src/services/smtp/smtp-service.ts | 3 +- .../templates/serviceTokenExpired.handlebars | 19 +++++++++++ 8 files changed, 116 insertions(+), 8 deletions(-) create mode 100644 backend/src/db/migrations/20250414203701_add-notification-flag-service-token.ts create mode 100644 backend/src/services/smtp/templates/serviceTokenExpired.handlebars diff --git a/backend/src/db/migrations/20250414203701_add-notification-flag-service-token.ts b/backend/src/db/migrations/20250414203701_add-notification-flag-service-token.ts new file mode 100644 index 000000000..dbfda96d1 --- /dev/null +++ b/backend/src/db/migrations/20250414203701_add-notification-flag-service-token.ts @@ -0,0 +1,27 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "notificationSent"); + if (!hasCol) { + await knex.schema.alterTable(TableName.ServiceToken, (t) => { + t.boolean("notificationSent").defaultTo(false); + }); + + // Update only tokens where expiresAt is before current time + await knex(TableName.ServiceToken) + .whereRaw(`${TableName.ServiceToken}."expiresAt" < NOW()`) + .whereNotNull("expiresAt") + .update({ notificationSent: true }); + } +} + +export async function down(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "notificationSent"); + if (hasCol) { + await knex.schema.alterTable(TableName.ServiceToken, (t) => { + t.dropColumn("notificationSent"); + }); + } +} diff --git a/backend/src/db/schemas/service-tokens.ts b/backend/src/db/schemas/service-tokens.ts index 720c8fd6f..40859fc15 100644 --- a/backend/src/db/schemas/service-tokens.ts +++ b/backend/src/db/schemas/service-tokens.ts @@ -21,7 +21,8 @@ export const ServiceTokensSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), createdBy: z.string(), - projectId: z.string() + projectId: z.string(), + notificationSent: z.boolean().default(false).nullable().optional() }); export type TServiceTokens = z.infer; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 743577d25..765a84b66 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1255,7 +1255,8 @@ export const registerRoutes = async ( userDAL, permissionService, projectDAL, - accessTokenQueue + accessTokenQueue, + smtpService }); const identityService = identityServiceFactory({ @@ -1415,7 +1416,8 @@ export const registerRoutes = async ( identityAccessTokenDAL, secretSharingDAL, secretVersionV2DAL: secretVersionV2BridgeDAL, - identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL + identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL, + serviceTokenService }); const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({ diff --git a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts index f0d579cf7..edd684783 100644 --- a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts +++ b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts @@ -10,6 +10,7 @@ import { TSecretVersionDALFactory } from "../secret/secret-version-dal"; import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal"; import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal"; import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; +import { TServiceTokenServiceFactory } from "../service-token/service-token-service"; type TDailyResourceCleanUpQueueServiceFactoryDep = { auditLogDAL: Pick; @@ -21,6 +22,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = { secretFolderVersionDAL: Pick; snapshotDAL: Pick; secretSharingDAL: Pick; + serviceTokenService: Pick; queueService: TQueueServiceFactory; }; @@ -36,7 +38,8 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ identityAccessTokenDAL, secretSharingDAL, secretVersionV2DAL, - identityUniversalAuthClientSecretDAL + identityUniversalAuthClientSecretDAL, + serviceTokenService }: TDailyResourceCleanUpQueueServiceFactoryDep) => { queueService.start(QueueName.DailyResourceCleanUp, async () => { logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`); @@ -50,6 +53,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ await secretVersionDAL.pruneExcessVersions(); await secretVersionV2DAL.pruneExcessVersions(); await secretFolderVersionDAL.pruneExcessVersions(); + await serviceTokenService.notifyExpiredTokens(); logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`); }); diff --git a/backend/src/services/service-token/service-token-dal.ts b/backend/src/services/service-token/service-token-dal.ts index ed9c5de7e..f9c457d32 100644 --- a/backend/src/services/service-token/service-token-dal.ts +++ b/backend/src/services/service-token/service-token-dal.ts @@ -28,5 +28,29 @@ export const serviceTokenDALFactory = (db: TDbClient) => { } }; - return { ...stOrm, findById }; + const findExpiredTokens = async (tx?: Knex) => { + try { + const docs: { name: string; projectName: string; createdByEmail: string; id: string; projectId: string }[] = + await (tx || db.replicaNode())(TableName.ServiceToken) + .leftJoin( + TableName.Users, + `${TableName.Users}.id`, + db.raw(`${TableName.ServiceToken}."createdBy"::uuid`) + ) + .join(TableName.Project, `${TableName.Project}.id`, `${TableName.ServiceToken}.projectId`) + .whereRaw( + `${TableName.ServiceToken}."expiresAt" < NOW() AND ${TableName.ServiceToken}."notificationSent" = false` + ) + .select(`${TableName.ServiceToken}.name`) + .select(`${TableName.ServiceToken}.id`) + .select(`${TableName.Project}.name as projectName`) + .select(`${TableName.ServiceToken}.projectId`) + .select(`${TableName.Users}.email as createdByEmail`); + + return docs; + } catch (err) { + throw new DatabaseError({ error: err, name: "FindById" }); + } + }; + return { ...stOrm, findById, findExpiredTokens }; }; diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index 9b87c29f8..dd2f834bb 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -17,6 +17,7 @@ import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-to import { ActorType } from "../auth/auth-type"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; +import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDALFactory } from "../user/user-dal"; import { TServiceTokenDALFactory } from "./service-token-dal"; import { @@ -33,6 +34,7 @@ type TServiceTokenServiceFactoryDep = { projectEnvDAL: Pick; projectDAL: Pick; accessTokenQueue: Pick; + smtpService: Pick; }; export type TServiceTokenServiceFactory = ReturnType; @@ -43,7 +45,8 @@ export const serviceTokenServiceFactory = ({ permissionService, projectEnvDAL, projectDAL, - accessTokenQueue + accessTokenQueue, + smtpService }: TServiceTokenServiceFactoryDep) => { const createServiceToken = async ({ iv, @@ -185,11 +188,38 @@ export const serviceTokenServiceFactory = ({ return { ...serviceToken, lastUsed: new Date(), orgId: project.orgId }; }; + const notifyExpiredTokens = async () => { + const appCfg = getConfig(); + + const expiredTokens = await serviceTokenDAL.findExpiredTokens(); + if (expiredTokens.length === 0) return; + + await Promise.all( + expiredTokens.map(async (token) => { + await smtpService + .sendMail({ + recipients: [token.createdByEmail], + subjectLine: "Service Token Expired", + template: SmtpTemplates.ServiceTokenExpired, + substitutions: { + tokenName: token.name, + projectName: token.projectName, + url: `${appCfg.SITE_URL}/secret-manager/${token.projectId}/access-management?selectedTab=service-tokens` + } + }) + .then(async () => { + await serviceTokenDAL.update({ id: token.id }, { notificationSent: true }); + }); + }) + ); + }; + return { createServiceToken, deleteServiceToken, getServiceToken, getProjectServiceTokens, - fnValidateServiceToken + fnValidateServiceToken, + notifyExpiredTokens }; }; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index 452283235..25f5f3949 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -43,7 +43,8 @@ export enum SmtpTemplates { SecretRequestCompleted = "secretRequestCompleted.handlebars", SecretRotationFailed = "secretRotationFailed.handlebars", ProjectAccessRequest = "projectAccess.handlebars", - OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars" + OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars", + ServiceTokenExpired = "serviceTokenExpired.handlebars" } export enum SmtpHost { diff --git a/backend/src/services/smtp/templates/serviceTokenExpired.handlebars b/backend/src/services/smtp/templates/serviceTokenExpired.handlebars new file mode 100644 index 000000000..c0d364430 --- /dev/null +++ b/backend/src/services/smtp/templates/serviceTokenExpired.handlebars @@ -0,0 +1,19 @@ + + + + + + Service Token Expired + + + +

Your Service Token has expired

+

Your service token "{{tokenName}}" has expired.

+ +

This token was being used to access the project "{{projectName}}". To ensure continued service, please create a new token as soon as possible.

+ + Create New Token + + {{emailFooter}} + + \ No newline at end of file From cac4f30ca82d4ff6a05118b24492f6f9523fe453 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski <63930039+carlosmonastyrski@users.noreply.github.com> Date: Mon, 14 Apr 2025 21:43:19 -0300 Subject: [PATCH 2/6] Update backend/src/services/service-token/service-token-dal.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- backend/src/services/service-token/service-token-dal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/service-token/service-token-dal.ts b/backend/src/services/service-token/service-token-dal.ts index f9c457d32..d9125c123 100644 --- a/backend/src/services/service-token/service-token-dal.ts +++ b/backend/src/services/service-token/service-token-dal.ts @@ -49,7 +49,7 @@ export const serviceTokenDALFactory = (db: TDbClient) => { return docs; } catch (err) { - throw new DatabaseError({ error: err, name: "FindById" }); + throw new DatabaseError({ error: err, name: "FindExpiredTokens" }); } }; return { ...stOrm, findById, findExpiredTokens }; From 86d7fca8fbfc94405e55271870daa607ef8af54b Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Mon, 14 Apr 2025 21:52:16 -0300 Subject: [PATCH 3/6] Add minor improvements to notifyExpiredTokens --- .../resource-cleanup/resource-cleanup-queue.ts | 2 +- .../src/services/service-token/service-token-dal.ts | 1 + .../services/service-token/service-token-service.ts | 12 +++++++----- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts index edd684783..5adb91801 100644 --- a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts +++ b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts @@ -77,7 +77,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ await queueService.queue(QueueName.DailyResourceCleanUp, QueueJobs.DailyResourceCleanUp, undefined, { delay: 5000, jobId: QueueName.DailyResourceCleanUp, - repeat: { pattern: "0 0 * * *", utc: true } + repeat: { pattern: "* * * * *", utc: true } }); }; diff --git a/backend/src/services/service-token/service-token-dal.ts b/backend/src/services/service-token/service-token-dal.ts index d9125c123..188cd86cd 100644 --- a/backend/src/services/service-token/service-token-dal.ts +++ b/backend/src/services/service-token/service-token-dal.ts @@ -41,6 +41,7 @@ export const serviceTokenDALFactory = (db: TDbClient) => { .whereRaw( `${TableName.ServiceToken}."expiresAt" < NOW() AND ${TableName.ServiceToken}."notificationSent" = false` ) + .whereNotNull(`${TableName.Users}.email`) .select(`${TableName.ServiceToken}.name`) .select(`${TableName.ServiceToken}.id`) .select(`${TableName.Project}.name as projectName`) diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index dd2f834bb..db44e6272 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -26,6 +26,7 @@ import { TGetServiceTokenInfoDTO, TProjectServiceTokensDTO } from "./service-token-types"; +import { logger } from "@app/lib/logger"; type TServiceTokenServiceFactoryDep = { serviceTokenDAL: TServiceTokenDALFactory; @@ -196,8 +197,8 @@ export const serviceTokenServiceFactory = ({ await Promise.all( expiredTokens.map(async (token) => { - await smtpService - .sendMail({ + try { + await smtpService.sendMail({ recipients: [token.createdByEmail], subjectLine: "Service Token Expired", template: SmtpTemplates.ServiceTokenExpired, @@ -206,10 +207,11 @@ export const serviceTokenServiceFactory = ({ projectName: token.projectName, url: `${appCfg.SITE_URL}/secret-manager/${token.projectId}/access-management?selectedTab=service-tokens` } - }) - .then(async () => { - await serviceTokenDAL.update({ id: token.id }, { notificationSent: true }); }); + await serviceTokenDAL.update({ id: token.id }, { notificationSent: true }); + } catch (error) { + logger.error(error, `Failed to send expiration notification for token ${token.id}:`); + } }) ); }; From ae8a78b8835a67dc1e69a85d5ea24ba93f2d33cc Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Tue, 15 Apr 2025 07:46:35 -0300 Subject: [PATCH 4/6] Fix cron schedule used to test --- backend/src/services/resource-cleanup/resource-cleanup-queue.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts index 5adb91801..edd684783 100644 --- a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts +++ b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts @@ -77,7 +77,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ await queueService.queue(QueueName.DailyResourceCleanUp, QueueJobs.DailyResourceCleanUp, undefined, { delay: 5000, jobId: QueueName.DailyResourceCleanUp, - repeat: { pattern: "* * * * *", utc: true } + repeat: { pattern: "0 0 * * *", utc: true } }); }; From 23a5a7a6242b29c38fc0d781be6b6e5e7fe26d3f Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Tue, 15 Apr 2025 18:31:05 -0300 Subject: [PATCH 5/6] Improvements on notify expired service tokens --- ...701_add-notification-flag-service-token.ts | 10 ++-- backend/src/db/schemas/service-tokens.ts | 2 +- .../service-token/service-token-dal.ts | 59 +++++++++++++------ .../service-token/service-token-service.ts | 6 +- .../templates/serviceTokenExpired.handlebars | 8 +-- 5 files changed, 53 insertions(+), 32 deletions(-) diff --git a/backend/src/db/migrations/20250414203701_add-notification-flag-service-token.ts b/backend/src/db/migrations/20250414203701_add-notification-flag-service-token.ts index dbfda96d1..9c5b4e730 100644 --- a/backend/src/db/migrations/20250414203701_add-notification-flag-service-token.ts +++ b/backend/src/db/migrations/20250414203701_add-notification-flag-service-token.ts @@ -3,25 +3,25 @@ import { Knex } from "knex"; import { TableName } from "../schemas"; export async function up(knex: Knex): Promise { - const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "notificationSent"); + const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "expiryNotificationSent"); if (!hasCol) { await knex.schema.alterTable(TableName.ServiceToken, (t) => { - t.boolean("notificationSent").defaultTo(false); + t.boolean("expiryNotificationSent").defaultTo(false); }); // Update only tokens where expiresAt is before current time await knex(TableName.ServiceToken) .whereRaw(`${TableName.ServiceToken}."expiresAt" < NOW()`) .whereNotNull("expiresAt") - .update({ notificationSent: true }); + .update({ expiryNotificationSent: true }); } } export async function down(knex: Knex): Promise { - const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "notificationSent"); + const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "expiryNotificationSent"); if (hasCol) { await knex.schema.alterTable(TableName.ServiceToken, (t) => { - t.dropColumn("notificationSent"); + t.dropColumn("expiryNotificationSent"); }); } } diff --git a/backend/src/db/schemas/service-tokens.ts b/backend/src/db/schemas/service-tokens.ts index 40859fc15..8ffddb10a 100644 --- a/backend/src/db/schemas/service-tokens.ts +++ b/backend/src/db/schemas/service-tokens.ts @@ -22,7 +22,7 @@ export const ServiceTokensSchema = z.object({ updatedAt: z.date(), createdBy: z.string(), projectId: z.string(), - notificationSent: z.boolean().default(false).nullable().optional() + expiryNotificationSent: z.boolean().default(false).nullable().optional() }); export type TServiceTokens = z.infer; diff --git a/backend/src/services/service-token/service-token-dal.ts b/backend/src/services/service-token/service-token-dal.ts index 188cd86cd..660d64a41 100644 --- a/backend/src/services/service-token/service-token-dal.ts +++ b/backend/src/services/service-token/service-token-dal.ts @@ -28,27 +28,48 @@ export const serviceTokenDALFactory = (db: TDbClient) => { } }; - const findExpiredTokens = async (tx?: Knex) => { + const findExpiredTokens = async (tx?: Knex, batchSize = 1000) => { try { - const docs: { name: string; projectName: string; createdByEmail: string; id: string; projectId: string }[] = - await (tx || db.replicaNode())(TableName.ServiceToken) - .leftJoin( - TableName.Users, - `${TableName.Users}.id`, - db.raw(`${TableName.ServiceToken}."createdBy"::uuid`) - ) - .join(TableName.Project, `${TableName.Project}.id`, `${TableName.ServiceToken}.projectId`) - .whereRaw( - `${TableName.ServiceToken}."expiresAt" < NOW() AND ${TableName.ServiceToken}."notificationSent" = false` - ) - .whereNotNull(`${TableName.Users}.email`) - .select(`${TableName.ServiceToken}.name`) - .select(`${TableName.ServiceToken}.id`) - .select(`${TableName.Project}.name as projectName`) - .select(`${TableName.ServiceToken}.projectId`) - .select(`${TableName.Users}.email as createdByEmail`); + const allDocs: { name: string; projectName: string; createdByEmail: string; id: string; projectId: string }[] = + []; + let offset = 0; + let hasMoreRecords = true; - return docs; + while (hasMoreRecords) { + // eslint-disable-next-line + const batch: { name: string; projectName: string; createdByEmail: string; id: string; projectId: string }[] = + // eslint-disable-next-line no-await-in-loop + await (tx || db.replicaNode())(TableName.ServiceToken) + .leftJoin( + TableName.Users, + `${TableName.Users}.id`, + db.raw(`${TableName.ServiceToken}."createdBy"::uuid`) + ) + .join(TableName.Project, `${TableName.Project}.id`, `${TableName.ServiceToken}.projectId`) + .whereRaw( + `${TableName.ServiceToken}."expiresAt" < NOW() + INTERVAL '1 day' AND ${TableName.ServiceToken}."expiryNotificationSent" = false` + ) + .whereNotNull(`${TableName.Users}.email`) + .select( + db.ref("id").withSchema(TableName.ServiceToken), + db.ref("name").withSchema(TableName.ServiceToken), + db.ref("projectId").withSchema(TableName.ServiceToken), + db.ref("createdBy").withSchema(TableName.ServiceToken), + db.ref("email").withSchema(TableName.Users).as("createdByEmail"), + db.ref("name").withSchema(TableName.Project).as("projectName") + ) + .limit(batchSize) + .offset(offset); + + if (batch.length === 0) { + hasMoreRecords = false; + } else { + allDocs.push(...batch); + offset += batchSize; + } + } + + return allDocs; } catch (err) { throw new DatabaseError({ error: err, name: "FindExpiredTokens" }); } diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index db44e6272..760ad4b79 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -12,6 +12,7 @@ import { } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue"; import { ActorType } from "../auth/auth-type"; @@ -26,7 +27,6 @@ import { TGetServiceTokenInfoDTO, TProjectServiceTokensDTO } from "./service-token-types"; -import { logger } from "@app/lib/logger"; type TServiceTokenServiceFactoryDep = { serviceTokenDAL: TServiceTokenDALFactory; @@ -200,7 +200,7 @@ export const serviceTokenServiceFactory = ({ try { await smtpService.sendMail({ recipients: [token.createdByEmail], - subjectLine: "Service Token Expired", + subjectLine: "Your Service Token is about to expire", template: SmtpTemplates.ServiceTokenExpired, substitutions: { tokenName: token.name, @@ -208,7 +208,7 @@ export const serviceTokenServiceFactory = ({ url: `${appCfg.SITE_URL}/secret-manager/${token.projectId}/access-management?selectedTab=service-tokens` } }); - await serviceTokenDAL.update({ id: token.id }, { notificationSent: true }); + await serviceTokenDAL.update({ id: token.id }, { expiryNotificationSent: true }); } catch (error) { logger.error(error, `Failed to send expiration notification for token ${token.id}:`); } diff --git a/backend/src/services/smtp/templates/serviceTokenExpired.handlebars b/backend/src/services/smtp/templates/serviceTokenExpired.handlebars index c0d364430..0b3c63dfb 100644 --- a/backend/src/services/smtp/templates/serviceTokenExpired.handlebars +++ b/backend/src/services/smtp/templates/serviceTokenExpired.handlebars @@ -3,14 +3,14 @@ - Service Token Expired + Service Token Expiring Soon -

Your Service Token has expired

-

Your service token "{{tokenName}}" has expired.

+

Your Service Token is about to expire

+

Your service token "{{tokenName}}" will expire within 24 hours.

-

This token was being used to access the project "{{projectName}}". To ensure continued service, please create a new token as soon as possible.

+

This token is currently being on project "{{projectName}}". If this token is still needed for your workflow, please create a new one before it expires.

Create New Token From 965084cc0c28b0c9c54316b8be7b5b5dfe8708d5 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Wed, 16 Apr 2025 12:48:00 -0300 Subject: [PATCH 6/6] notifyExpiredTokens fixes --- .../resource-cleanup-queue.ts | 4 +- .../service-token/service-token-dal.ts | 67 +++++++------------ .../service-token/service-token-service.ts | 63 ++++++++++------- .../templates/serviceTokenExpired.handlebars | 4 +- 4 files changed, 70 insertions(+), 68 deletions(-) diff --git a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts index edd684783..32f180636 100644 --- a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts +++ b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts @@ -22,7 +22,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = { secretFolderVersionDAL: Pick; snapshotDAL: Pick; secretSharingDAL: Pick; - serviceTokenService: Pick; + serviceTokenService: Pick; queueService: TQueueServiceFactory; }; @@ -53,7 +53,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ await secretVersionDAL.pruneExcessVersions(); await secretVersionV2DAL.pruneExcessVersions(); await secretFolderVersionDAL.pruneExcessVersions(); - await serviceTokenService.notifyExpiredTokens(); + await serviceTokenService.notifyExpiringTokens(); logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`); }); diff --git a/backend/src/services/service-token/service-token-dal.ts b/backend/src/services/service-token/service-token-dal.ts index 660d64a41..adb2f325a 100644 --- a/backend/src/services/service-token/service-token-dal.ts +++ b/backend/src/services/service-token/service-token-dal.ts @@ -28,51 +28,36 @@ export const serviceTokenDALFactory = (db: TDbClient) => { } }; - const findExpiredTokens = async (tx?: Knex, batchSize = 1000) => { + const findExpiringTokens = async (tx?: Knex, batchSize = 500, offset = 0) => { try { - const allDocs: { name: string; projectName: string; createdByEmail: string; id: string; projectId: string }[] = - []; - let offset = 0; - let hasMoreRecords = true; + const batch: { name: string; projectName: string; createdByEmail: string; id: string; projectId: string }[] = + await (tx || db.replicaNode())(TableName.ServiceToken) + .leftJoin( + TableName.Users, + `${TableName.Users}.id`, + db.raw(`${TableName.ServiceToken}."createdBy"::uuid`) + ) + .join(TableName.Project, `${TableName.Project}.id`, `${TableName.ServiceToken}.projectId`) + .whereRaw( + `${TableName.ServiceToken}."expiresAt" < NOW() + INTERVAL '1 day' AND ${TableName.ServiceToken}."expiryNotificationSent" = false` + ) + .whereNotNull(`${TableName.Users}.email`) + .select( + db.ref("id").withSchema(TableName.ServiceToken), + db.ref("name").withSchema(TableName.ServiceToken), + db.ref("projectId").withSchema(TableName.ServiceToken), + db.ref("createdBy").withSchema(TableName.ServiceToken), + db.ref("email").withSchema(TableName.Users).as("createdByEmail"), + db.ref("name").withSchema(TableName.Project).as("projectName") + ) + .limit(batchSize) + .offset(offset); - while (hasMoreRecords) { - // eslint-disable-next-line - const batch: { name: string; projectName: string; createdByEmail: string; id: string; projectId: string }[] = - // eslint-disable-next-line no-await-in-loop - await (tx || db.replicaNode())(TableName.ServiceToken) - .leftJoin( - TableName.Users, - `${TableName.Users}.id`, - db.raw(`${TableName.ServiceToken}."createdBy"::uuid`) - ) - .join(TableName.Project, `${TableName.Project}.id`, `${TableName.ServiceToken}.projectId`) - .whereRaw( - `${TableName.ServiceToken}."expiresAt" < NOW() + INTERVAL '1 day' AND ${TableName.ServiceToken}."expiryNotificationSent" = false` - ) - .whereNotNull(`${TableName.Users}.email`) - .select( - db.ref("id").withSchema(TableName.ServiceToken), - db.ref("name").withSchema(TableName.ServiceToken), - db.ref("projectId").withSchema(TableName.ServiceToken), - db.ref("createdBy").withSchema(TableName.ServiceToken), - db.ref("email").withSchema(TableName.Users).as("createdByEmail"), - db.ref("name").withSchema(TableName.Project).as("projectName") - ) - .limit(batchSize) - .offset(offset); - - if (batch.length === 0) { - hasMoreRecords = false; - } else { - allDocs.push(...batch); - offset += batchSize; - } - } - - return allDocs; + return batch; } catch (err) { throw new DatabaseError({ error: err, name: "FindExpiredTokens" }); } }; - return { ...stOrm, findById, findExpiredTokens }; + + return { ...stOrm, findById, findExpiringTokens }; }; diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index 760ad4b79..bbd306bb5 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -189,31 +189,48 @@ export const serviceTokenServiceFactory = ({ return { ...serviceToken, lastUsed: new Date(), orgId: project.orgId }; }; - const notifyExpiredTokens = async () => { + const notifyExpiringTokens = async () => { const appCfg = getConfig(); + let processedCount = 0; + let hasMoreRecords = true; + let offset = 0; + const batchSize = 500; - const expiredTokens = await serviceTokenDAL.findExpiredTokens(); - if (expiredTokens.length === 0) return; + while (hasMoreRecords) { + // eslint-disable-next-line no-await-in-loop + const expiringTokens = await serviceTokenDAL.findExpiringTokens(undefined, batchSize, offset); - await Promise.all( - expiredTokens.map(async (token) => { - try { - await smtpService.sendMail({ - recipients: [token.createdByEmail], - subjectLine: "Your Service Token is about to expire", - template: SmtpTemplates.ServiceTokenExpired, - substitutions: { - tokenName: token.name, - projectName: token.projectName, - url: `${appCfg.SITE_URL}/secret-manager/${token.projectId}/access-management?selectedTab=service-tokens` - } - }); - await serviceTokenDAL.update({ id: token.id }, { expiryNotificationSent: true }); - } catch (error) { - logger.error(error, `Failed to send expiration notification for token ${token.id}:`); - } - }) - ); + if (expiringTokens.length === 0) { + hasMoreRecords = false; + break; + } + + // eslint-disable-next-line no-await-in-loop + await Promise.all( + expiringTokens.map(async (token) => { + try { + await smtpService.sendMail({ + recipients: [token.createdByEmail], + subjectLine: "Service Token Expiry Notice", + template: SmtpTemplates.ServiceTokenExpired, + substitutions: { + tokenName: token.name, + projectName: token.projectName, + url: `${appCfg.SITE_URL}/secret-manager/${token.projectId}/access-management?selectedTab=service-tokens` + } + }); + await serviceTokenDAL.update({ id: token.id }, { expiryNotificationSent: true }); + } catch (error) { + logger.error(error, `Failed to send expiration notification for token ${token.id}:`); + } + }) + ); + + processedCount += expiringTokens.length; + offset += batchSize; + } + + return processedCount; }; return { @@ -222,6 +239,6 @@ export const serviceTokenServiceFactory = ({ getServiceToken, getProjectServiceTokens, fnValidateServiceToken, - notifyExpiredTokens + notifyExpiringTokens }; }; diff --git a/backend/src/services/smtp/templates/serviceTokenExpired.handlebars b/backend/src/services/smtp/templates/serviceTokenExpired.handlebars index 0b3c63dfb..199150c05 100644 --- a/backend/src/services/smtp/templates/serviceTokenExpired.handlebars +++ b/backend/src/services/smtp/templates/serviceTokenExpired.handlebars @@ -7,10 +7,10 @@ -

Your Service Token is about to expire

+

Service Token Expiry Notice

Your service token "{{tokenName}}" will expire within 24 hours.

-

This token is currently being on project "{{projectName}}". If this token is still needed for your workflow, please create a new one before it expires.

+

This token is currently being used on project "{{projectName}}". If this token is still needed for your workflow, please create a new one before it expires.

Create New Token