mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Improvements on notify expired service tokens
This commit is contained in:
@@ -3,25 +3,25 @@ import { Knex } from "knex";
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
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<void> {
|
||||
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");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<typeof ServiceTokensSchema>;
|
||||
|
||||
@@ -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<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() 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<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);
|
||||
|
||||
if (batch.length === 0) {
|
||||
hasMoreRecords = false;
|
||||
} else {
|
||||
allDocs.push(...batch);
|
||||
offset += batchSize;
|
||||
}
|
||||
}
|
||||
|
||||
return allDocs;
|
||||
} catch (err) {
|
||||
throw new DatabaseError({ error: err, name: "FindExpiredTokens" });
|
||||
}
|
||||
|
||||
@@ -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}:`);
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Service Token Expired</title>
|
||||
<title>Service Token Expiring Soon</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Your Service Token has expired</h2>
|
||||
<p>Your service token <strong>"{{tokenName}}"</strong> has expired.</p>
|
||||
<h2>Your Service Token is about to expire</h2>
|
||||
<p>Your service token <strong>"{{tokenName}}"</strong> will expire within 24 hours.</p>
|
||||
|
||||
<p>This token was being used to access the project "{{projectName}}". To ensure continued service, please create a new token as soon as possible.</p>
|
||||
<p>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.</p>
|
||||
|
||||
<a href="{{url}}">Create New Token</a>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user