mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 18:28:12 +00:00
Add notification on Service Token expiration
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
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");
|
||||||
|
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<void> {
|
||||||
|
const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "notificationSent");
|
||||||
|
if (hasCol) {
|
||||||
|
await knex.schema.alterTable(TableName.ServiceToken, (t) => {
|
||||||
|
t.dropColumn("notificationSent");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,8 @@ export const ServiceTokensSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
createdBy: z.string(),
|
createdBy: z.string(),
|
||||||
projectId: z.string()
|
projectId: z.string(),
|
||||||
|
notificationSent: z.boolean().default(false).nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TServiceTokens = z.infer<typeof ServiceTokensSchema>;
|
export type TServiceTokens = z.infer<typeof ServiceTokensSchema>;
|
||||||
|
|||||||
@@ -1255,7 +1255,8 @@ export const registerRoutes = async (
|
|||||||
userDAL,
|
userDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
accessTokenQueue
|
accessTokenQueue,
|
||||||
|
smtpService
|
||||||
});
|
});
|
||||||
|
|
||||||
const identityService = identityServiceFactory({
|
const identityService = identityServiceFactory({
|
||||||
@@ -1415,7 +1416,8 @@ export const registerRoutes = async (
|
|||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
secretSharingDAL,
|
secretSharingDAL,
|
||||||
secretVersionV2DAL: secretVersionV2BridgeDAL,
|
secretVersionV2DAL: secretVersionV2BridgeDAL,
|
||||||
identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL
|
identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL,
|
||||||
|
serviceTokenService
|
||||||
});
|
});
|
||||||
|
|
||||||
const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({
|
const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
|||||||
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
||||||
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
|
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
|
||||||
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
|
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
|
||||||
|
import { TServiceTokenServiceFactory } from "../service-token/service-token-service";
|
||||||
|
|
||||||
type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
||||||
auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">;
|
auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">;
|
||||||
@@ -21,6 +22,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
|||||||
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
|
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
|
||||||
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
|
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
|
||||||
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets" | "pruneExpiredSecretRequests">;
|
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets" | "pruneExpiredSecretRequests">;
|
||||||
|
serviceTokenService: Pick<TServiceTokenServiceFactory, "notifyExpiredTokens">;
|
||||||
queueService: TQueueServiceFactory;
|
queueService: TQueueServiceFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,7 +38,8 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
|||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
secretSharingDAL,
|
secretSharingDAL,
|
||||||
secretVersionV2DAL,
|
secretVersionV2DAL,
|
||||||
identityUniversalAuthClientSecretDAL
|
identityUniversalAuthClientSecretDAL,
|
||||||
|
serviceTokenService
|
||||||
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
|
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
|
||||||
queueService.start(QueueName.DailyResourceCleanUp, async () => {
|
queueService.start(QueueName.DailyResourceCleanUp, async () => {
|
||||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
|
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
|
||||||
@@ -50,6 +53,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
|||||||
await secretVersionDAL.pruneExcessVersions();
|
await secretVersionDAL.pruneExcessVersions();
|
||||||
await secretVersionV2DAL.pruneExcessVersions();
|
await secretVersionV2DAL.pruneExcessVersions();
|
||||||
await secretFolderVersionDAL.pruneExcessVersions();
|
await secretFolderVersionDAL.pruneExcessVersions();
|
||||||
|
await serviceTokenService.notifyExpiredTokens();
|
||||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
|
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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<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`
|
||||||
|
)
|
||||||
|
.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 };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-to
|
|||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TServiceTokenDALFactory } from "./service-token-dal";
|
import { TServiceTokenDALFactory } from "./service-token-dal";
|
||||||
import {
|
import {
|
||||||
@@ -33,6 +34,7 @@ type TServiceTokenServiceFactoryDep = {
|
|||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findBySlugs">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findBySlugs">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
accessTokenQueue: Pick<TAccessTokenQueueServiceFactory, "updateServiceTokenStatus">;
|
accessTokenQueue: Pick<TAccessTokenQueueServiceFactory, "updateServiceTokenStatus">;
|
||||||
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TServiceTokenServiceFactory = ReturnType<typeof serviceTokenServiceFactory>;
|
export type TServiceTokenServiceFactory = ReturnType<typeof serviceTokenServiceFactory>;
|
||||||
@@ -43,7 +45,8 @@ export const serviceTokenServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
accessTokenQueue
|
accessTokenQueue,
|
||||||
|
smtpService
|
||||||
}: TServiceTokenServiceFactoryDep) => {
|
}: TServiceTokenServiceFactoryDep) => {
|
||||||
const createServiceToken = async ({
|
const createServiceToken = async ({
|
||||||
iv,
|
iv,
|
||||||
@@ -185,11 +188,38 @@ export const serviceTokenServiceFactory = ({
|
|||||||
return { ...serviceToken, lastUsed: new Date(), orgId: project.orgId };
|
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 {
|
return {
|
||||||
createServiceToken,
|
createServiceToken,
|
||||||
deleteServiceToken,
|
deleteServiceToken,
|
||||||
getServiceToken,
|
getServiceToken,
|
||||||
getProjectServiceTokens,
|
getProjectServiceTokens,
|
||||||
fnValidateServiceToken
|
fnValidateServiceToken,
|
||||||
|
notifyExpiredTokens
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ export enum SmtpTemplates {
|
|||||||
SecretRequestCompleted = "secretRequestCompleted.handlebars",
|
SecretRequestCompleted = "secretRequestCompleted.handlebars",
|
||||||
SecretRotationFailed = "secretRotationFailed.handlebars",
|
SecretRotationFailed = "secretRotationFailed.handlebars",
|
||||||
ProjectAccessRequest = "projectAccess.handlebars",
|
ProjectAccessRequest = "projectAccess.handlebars",
|
||||||
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars"
|
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars",
|
||||||
|
ServiceTokenExpired = "serviceTokenExpired.handlebars"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SmtpHost {
|
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 Expired</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>Your Service Token has expired</h2>
|
||||||
|
<p>Your service token <strong>"{{tokenName}}"</strong> has expired.</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>
|
||||||
|
|
||||||
|
<a href="{{url}}">Create New Token</a>
|
||||||
|
|
||||||
|
{{emailFooter}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user