diff --git a/backend/src/ee/services/audit-log/audit-log-dal.ts b/backend/src/ee/services/audit-log/audit-log-dal.ts index 316cf34a5..9d181e1aa 100644 --- a/backend/src/ee/services/audit-log/audit-log-dal.ts +++ b/backend/src/ee/services/audit-log/audit-log-dal.ts @@ -75,15 +75,19 @@ export const auditLogDALFactory = (db: TDbClient) => { .del() .returning("id"); numberOfRetryOnFailure = 0; // reset - // eslint-disable-next-line no-await-in-loop - await new Promise((resolve) => { - setTimeout(resolve, 100); // time to breathe for db - }); } catch (error) { numberOfRetryOnFailure += 1; logger.error(error, "Failed to delete audit log on pruning"); + } finally { + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, 10); // time to breathe for db + }); } - } while (deletedAuditLogIds.length > 0 && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE); + } while ( + deletedAuditLogIds.length > 0 || + (numberOfRetryOnFailure > 0 && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE) + ); }; return { ...auditLogOrm, pruneAuditLog, find }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 2902dad94..5116261d5 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1037,7 +1037,8 @@ export const registerRoutes = async ( snapshotDAL, identityAccessTokenDAL, secretSharingDAL, - secretVersionV2DAL: secretVersionV2BridgeDAL + secretVersionV2DAL: secretVersionV2BridgeDAL, + identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL }); const oidcService = oidcConfigServiceFactory({ diff --git a/backend/src/services/identity-ua/identity-ua-client-secret-dal.ts b/backend/src/services/identity-ua/identity-ua-client-secret-dal.ts index 6310da97a..4e35f6232 100644 --- a/backend/src/services/identity-ua/identity-ua-client-secret-dal.ts +++ b/backend/src/services/identity-ua/identity-ua-client-secret-dal.ts @@ -4,6 +4,7 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; +import { logger } from "@app/lib/logger"; export type TIdentityUaClientSecretDALFactory = ReturnType; @@ -23,5 +24,58 @@ export const identityUaClientSecretDALFactory = (db: TDbClient) => { } }; - return { ...uaClientSecretOrm, incrementUsage }; + const removeExpiredClientSecrets = async (tx?: Knex) => { + const BATCH_SIZE = 10000; + const MAX_RETRY_ON_FAILURE = 3; + + let deletedClientSecret: { id: string }[] = []; + let numberOfRetryOnFailure = 0; + + do { + try { + const findExpiredClientSecretQuery = (tx || db)(TableName.IdentityUaClientSecret) + .where({ + isClientSecretRevoked: true + }) + .orWhere((qb) => { + void qb + .where("clientSecretNumUses", ">", 0) + .andWhere( + "clientSecretNumUses", + ">=", + db.ref("clientSecretNumUsesLimit").withSchema(TableName.IdentityUaClientSecret) + ); + }) + .orWhere((qb) => { + void qb + .where("clientSecretTTL", ">", 0) + .andWhereRaw( + `"${TableName.IdentityUaClientSecret}"."createdAt" + make_interval(secs => "${TableName.IdentityUaClientSecret}"."clientSecretTTL") < NOW()` + ); + }) + .select("id") + .limit(BATCH_SIZE); + + // eslint-disable-next-line no-await-in-loop + deletedClientSecret = await (tx || db)(TableName.IdentityUaClientSecret) + .whereIn("id", findExpiredClientSecretQuery) + .del() + .returning("id"); + numberOfRetryOnFailure = 0; // reset + } catch (error) { + numberOfRetryOnFailure += 1; + logger.error(error, "Failed to delete client secret on pruning"); + } finally { + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, 10); // time to breathe for db + }); + } + } while ( + deletedClientSecret.length > 0 || + (numberOfRetryOnFailure > 0 && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE) + ); + }; + + return { ...uaClientSecretOrm, incrementUsage, removeExpiredClientSecrets }; }; diff --git a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts index cb9756dd4..dab70806f 100644 --- a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts +++ b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts @@ -4,6 +4,7 @@ import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; +import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal"; import { TSecretVersionDALFactory } from "../secret/secret-version-dal"; import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal"; import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal"; @@ -12,6 +13,7 @@ import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-d type TDailyResourceCleanUpQueueServiceFactoryDep = { auditLogDAL: Pick; identityAccessTokenDAL: Pick; + identityUniversalAuthClientSecretDAL: Pick; secretVersionDAL: Pick; secretVersionV2DAL: Pick; secretFolderVersionDAL: Pick; @@ -30,12 +32,14 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ secretFolderVersionDAL, identityAccessTokenDAL, secretSharingDAL, - secretVersionV2DAL + secretVersionV2DAL, + identityUniversalAuthClientSecretDAL }: TDailyResourceCleanUpQueueServiceFactoryDep) => { queueService.start(QueueName.DailyResourceCleanUp, async () => { logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`); await auditLogDAL.pruneAuditLog(); await identityAccessTokenDAL.removeExpiredTokens(); + await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets(); await secretSharingDAL.pruneExpiredSharedSecrets(); await snapshotDAL.pruneExcessSnapshots(); await secretVersionDAL.pruneExcessVersions();