mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 23:52:09 +00:00
Merge pull request #3419 from Infisical/feat/notifyOnServiceTokenExpiration
Add notification on Service Token expiration
This commit is contained in:
@@ -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<TAuditLogDALFactory, "pruneAuditLog">;
|
||||
@@ -21,6 +22,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
||||
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
|
||||
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
|
||||
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets" | "pruneExpiredSecretRequests">;
|
||||
serviceTokenService: Pick<TServiceTokenServiceFactory, "notifyExpiringTokens">;
|
||||
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.notifyExpiringTokens();
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
|
||||
});
|
||||
|
||||
|
||||
@@ -28,5 +28,36 @@ export const serviceTokenDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { ...stOrm, findById };
|
||||
const findExpiringTokens = async (tx?: Knex, batchSize = 500, offset = 0) => {
|
||||
try {
|
||||
const batch: { name: string; projectName: string; createdByEmail: string; id: string; projectId: string }[] =
|
||||
await (tx || db.replicaNode())(TableName.ServiceToken)
|
||||
.leftJoin<TUsers>(
|
||||
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);
|
||||
|
||||
return batch;
|
||||
} catch (err) {
|
||||
throw new DatabaseError({ error: err, name: "FindExpiredTokens" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...stOrm, findById, findExpiringTokens };
|
||||
};
|
||||
|
||||
@@ -12,11 +12,13 @@ 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";
|
||||
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 +35,7 @@ type TServiceTokenServiceFactoryDep = {
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findBySlugs">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
accessTokenQueue: Pick<TAccessTokenQueueServiceFactory, "updateServiceTokenStatus">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
};
|
||||
|
||||
export type TServiceTokenServiceFactory = ReturnType<typeof serviceTokenServiceFactory>;
|
||||
@@ -43,7 +46,8 @@ export const serviceTokenServiceFactory = ({
|
||||
permissionService,
|
||||
projectEnvDAL,
|
||||
projectDAL,
|
||||
accessTokenQueue
|
||||
accessTokenQueue,
|
||||
smtpService
|
||||
}: TServiceTokenServiceFactoryDep) => {
|
||||
const createServiceToken = async ({
|
||||
iv,
|
||||
@@ -185,11 +189,56 @@ export const serviceTokenServiceFactory = ({
|
||||
return { ...serviceToken, lastUsed: new Date(), orgId: project.orgId };
|
||||
};
|
||||
|
||||
const notifyExpiringTokens = async () => {
|
||||
const appCfg = getConfig();
|
||||
let processedCount = 0;
|
||||
let hasMoreRecords = true;
|
||||
let offset = 0;
|
||||
const batchSize = 500;
|
||||
|
||||
while (hasMoreRecords) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const expiringTokens = await serviceTokenDAL.findExpiringTokens(undefined, batchSize, offset);
|
||||
|
||||
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 {
|
||||
createServiceToken,
|
||||
deleteServiceToken,
|
||||
getServiceToken,
|
||||
getProjectServiceTokens,
|
||||
fnValidateServiceToken
|
||||
fnValidateServiceToken,
|
||||
notifyExpiringTokens
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Service Token Expiring Soon</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Service Token Expiry Notice</h2>
|
||||
<p>Your service token <strong>"{{tokenName}}"</strong> will expire within 24 hours.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<a href="{{url}}">Create New Token</a>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user