mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 19:44:52 +00:00
Finish docs for pki alerting + expose endpoints
This commit is contained in:
@@ -1,12 +1,84 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
import { PkiItemType } from "../pki-collection/pki-collection-types";
|
||||
|
||||
export type TPkiAlertDALFactory = ReturnType<typeof pkiAlertDALFactory>;
|
||||
|
||||
export const pkiAlertDALFactory = (db: TDbClient) => {
|
||||
const pkiAlertOrm = ormify(db, TableName.PkiAlert);
|
||||
|
||||
const getExpiringPkiCollectionItemsForAlerting = async () => {
|
||||
try {
|
||||
type AlertItem = {
|
||||
type: PkiItemType;
|
||||
id: string; // id of the CA or certificate
|
||||
expiryDate: Date;
|
||||
serialNumber: string;
|
||||
friendlyName: string;
|
||||
pkiCollectionId: string;
|
||||
alertId: string;
|
||||
alertName: string;
|
||||
alertBeforeDays: number;
|
||||
recipientEmails: string;
|
||||
};
|
||||
|
||||
// gets CAs and certificates as part of PKI collection items
|
||||
const combinedQuery = db
|
||||
.replicaNode()
|
||||
.select(
|
||||
db.raw("? as type", [PkiItemType.CA]),
|
||||
`${PkiItemType.CA}.id`,
|
||||
`${PkiItemType.CA}.notAfter as expiryDate`,
|
||||
`${PkiItemType.CA}.serialNumber`,
|
||||
`${PkiItemType.CA}.friendlyName`,
|
||||
"pci.pkiCollectionId"
|
||||
)
|
||||
.from(`${TableName.CertificateAuthority} as ${PkiItemType.CA}`)
|
||||
.join(`${TableName.PkiCollectionItem} as pci`, `${PkiItemType.CA}.id`, "pci.caId")
|
||||
.unionAll((qb) => {
|
||||
void qb
|
||||
.select(
|
||||
db.raw("? as type", [PkiItemType.CERTIFICATE]),
|
||||
`${PkiItemType.CERTIFICATE}.id`,
|
||||
`${PkiItemType.CERTIFICATE}.notAfter as expiryDate`,
|
||||
`${PkiItemType.CERTIFICATE}.serialNumber`,
|
||||
`${PkiItemType.CERTIFICATE}.friendlyName`,
|
||||
"pci.pkiCollectionId"
|
||||
)
|
||||
.from(`${TableName.Certificate} as ${PkiItemType.CERTIFICATE}`)
|
||||
.join(`${TableName.PkiCollectionItem} as pci`, `${PkiItemType.CERTIFICATE}.id`, "pci.certId");
|
||||
});
|
||||
|
||||
/**
|
||||
* Gets alerts to send based on alertBeforeDays on PKI alerts connected to PKI collection items
|
||||
* Note: Results are clamped to 1-day window to avoid sending multiple alerts for the same item
|
||||
*/
|
||||
const alertQuery = db
|
||||
.replicaNode()
|
||||
.select("combined.*", "pa.id as alertId", "pa.name as alertName", "pa.alertBeforeDays", "pa.recipientEmails")
|
||||
.from(db.raw("(?) as combined", [combinedQuery]))
|
||||
.join(`${TableName.PkiAlert} as pa`, "combined.pkiCollectionId", "pa.pkiCollectionId")
|
||||
.whereRaw(
|
||||
`
|
||||
combined."expiryDate" <= CURRENT_TIMESTAMP + (pa."alertBeforeDays" * INTERVAL '1 day')
|
||||
AND combined."expiryDate" > CURRENT_TIMESTAMP + ((pa."alertBeforeDays" - 1) * INTERVAL '1 day')
|
||||
`
|
||||
)
|
||||
.orderBy("combined.expiryDate");
|
||||
|
||||
const results = (await alertQuery) as AlertItem[];
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Get expiring PKI collection items for alerting" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getExpiringPkiCollectionItemsForAlerting,
|
||||
...pkiAlertOrm
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,7 +3,10 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
|
||||
import { pkiItemTypeToNameMap } from "@app/services/pki-collection/pki-collection-types";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
|
||||
import { TPkiAlertDALFactory } from "./pki-alert-dal";
|
||||
import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./pki-alert-types";
|
||||
@@ -12,6 +15,7 @@ type TPkiAlertServiceFactoryDep = {
|
||||
pkiAlertDAL: TPkiAlertDALFactory;
|
||||
pkiCollectionDAL: TPkiCollectionDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
};
|
||||
|
||||
export type TPkiAlertServiceFactory = ReturnType<typeof pkiAlertServiceFactory>;
|
||||
@@ -19,8 +23,42 @@ export type TPkiAlertServiceFactory = ReturnType<typeof pkiAlertServiceFactory>;
|
||||
export const pkiAlertServiceFactory = ({
|
||||
pkiAlertDAL,
|
||||
pkiCollectionDAL,
|
||||
permissionService
|
||||
permissionService,
|
||||
smtpService
|
||||
}: TPkiAlertServiceFactoryDep) => {
|
||||
const sendPkiItemExpiryNotices = async () => {
|
||||
const allAlertItems = await pkiAlertDAL.getExpiringPkiCollectionItemsForAlerting();
|
||||
|
||||
const flattenedResults = allAlertItems.flatMap(({ recipientEmails, ...item }) =>
|
||||
recipientEmails.split(",").map((email) => ({
|
||||
...item,
|
||||
recipientEmail: email.trim()
|
||||
}))
|
||||
);
|
||||
|
||||
const groupedByEmail = groupBy(flattenedResults, (item) => item.recipientEmail);
|
||||
|
||||
for await (const [email, items] of Object.entries(groupedByEmail)) {
|
||||
const groupedByAlert = groupBy(items, (item) => item.alertId);
|
||||
for await (const [, alertItems] of Object.entries(groupedByAlert)) {
|
||||
await smtpService.sendMail({
|
||||
recipients: [email],
|
||||
subjectLine: `Infisical CA/Certificate expiration notice: ${alertItems[0].alertName}`,
|
||||
substitutions: {
|
||||
alertName: alertItems[0].alertName,
|
||||
alertBeforeDays: items[0].alertBeforeDays,
|
||||
items: alertItems.map((alertItem) => ({
|
||||
...alertItem,
|
||||
type: pkiItemTypeToNameMap[alertItem.type],
|
||||
expiryDate: new Date(alertItem.expiryDate).toString()
|
||||
}))
|
||||
},
|
||||
template: SmtpTemplates.PkiExpirationAlert
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createPkiAlert = async ({
|
||||
projectId,
|
||||
name,
|
||||
@@ -108,7 +146,7 @@ export const pkiAlertServiceFactory = ({
|
||||
name,
|
||||
alertBeforeDays,
|
||||
...(pkiCollectionId && { pkiCollectionId }),
|
||||
...(emails && { recipientEmails: emails.join(",") }) // TODO: standardize recipient emails
|
||||
...(emails && { recipientEmails: emails.join(",") })
|
||||
});
|
||||
|
||||
return alert;
|
||||
@@ -132,6 +170,7 @@ export const pkiAlertServiceFactory = ({
|
||||
};
|
||||
|
||||
return {
|
||||
sendPkiItemExpiryNotices,
|
||||
createPkiAlert,
|
||||
getPkiAlertById,
|
||||
updatePkiAlert,
|
||||
|
||||
@@ -1,13 +1,72 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { TableName, TPkiCollectionItems } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
import { PkiItemType } from "./pki-collection-types";
|
||||
|
||||
export type TPkiCollectionItemDALFactory = ReturnType<typeof pkiCollectionItemDALFactory>;
|
||||
|
||||
export const pkiCollectionItemDALFactory = (db: TDbClient) => {
|
||||
const pkiCollectionItemOrm = ormify(db, TableName.PkiCollectionItem);
|
||||
|
||||
const findPkiCollectionItems = async ({
|
||||
collectionId,
|
||||
type,
|
||||
offset,
|
||||
limit
|
||||
}: {
|
||||
collectionId: string;
|
||||
type?: PkiItemType;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
}) => {
|
||||
try {
|
||||
const query = db
|
||||
.replicaNode()(TableName.PkiCollectionItem)
|
||||
.select(
|
||||
"pki_collection_items.*",
|
||||
db.raw(
|
||||
`COALESCE("${TableName.CertificateAuthority}"."notBefore", "${TableName.Certificate}"."notBefore") as "notBefore"`
|
||||
),
|
||||
db.raw(
|
||||
`COALESCE("${TableName.CertificateAuthority}"."notAfter", "${TableName.Certificate}"."notAfter") as "notAfter"`
|
||||
),
|
||||
db.raw(
|
||||
`COALESCE("${TableName.CertificateAuthority}"."friendlyName", "${TableName.Certificate}"."friendlyName") as "friendlyName"`
|
||||
)
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.CertificateAuthority,
|
||||
`${TableName.PkiCollectionItem}.caId`,
|
||||
`${TableName.CertificateAuthority}.id`
|
||||
)
|
||||
.leftJoin(TableName.Certificate, `${TableName.PkiCollectionItem}.certId`, `${TableName.Certificate}.id`)
|
||||
.where((builder) => {
|
||||
void builder.where(`${TableName.PkiCollectionItem}.pkiCollectionId`, collectionId);
|
||||
if (type === PkiItemType.CA) {
|
||||
void builder.whereNull(`${TableName.PkiCollectionItem}.certId`);
|
||||
} else if (type === PkiItemType.CERTIFICATE) {
|
||||
void builder.whereNull(`${TableName.PkiCollectionItem}.caId`);
|
||||
}
|
||||
});
|
||||
|
||||
if (offset) {
|
||||
void query.offset(offset);
|
||||
}
|
||||
if (limit) {
|
||||
void query.limit(limit);
|
||||
}
|
||||
|
||||
void query.orderBy(`${TableName.PkiCollectionItem}.createdAt`, "desc");
|
||||
|
||||
const result = await query;
|
||||
return result as (TPkiCollectionItems & { notAfter: Date; notBefore: Date; friendlyName: string })[];
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find all PKI collection items" });
|
||||
}
|
||||
};
|
||||
|
||||
const countItemsInPkiCollection = async (collectionId: string) => {
|
||||
try {
|
||||
interface CountResult {
|
||||
@@ -28,6 +87,7 @@ export const pkiCollectionItemDALFactory = (db: TDbClient) => {
|
||||
|
||||
return {
|
||||
...pkiCollectionItemOrm,
|
||||
findPkiCollectionItems,
|
||||
countItemsInPkiCollection
|
||||
};
|
||||
};
|
||||
|
||||
@@ -144,6 +144,7 @@ export const pkiCollectionServiceFactory = ({
|
||||
|
||||
const getPkiCollectionItems = async ({
|
||||
collectionId,
|
||||
type,
|
||||
offset = 0,
|
||||
limit = 25,
|
||||
actorId,
|
||||
@@ -164,16 +165,23 @@ export const pkiCollectionServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
|
||||
|
||||
const pkiCollectionItems = await pkiCollectionItemDAL.find(
|
||||
{ pkiCollectionId: collectionId },
|
||||
{ offset, limit, sort: [["createdAt", "desc"]] }
|
||||
);
|
||||
const pkiCollectionItems = await pkiCollectionItemDAL.findPkiCollectionItems({
|
||||
collectionId,
|
||||
type,
|
||||
offset,
|
||||
limit
|
||||
});
|
||||
|
||||
const count = await pkiCollectionItemDAL.countItemsInPkiCollection(collectionId);
|
||||
|
||||
return {
|
||||
pkiCollection,
|
||||
pkiCollectionItems: pkiCollectionItems.map(transformPkiCollectionItem),
|
||||
pkiCollectionItems: pkiCollectionItems.map((p) => ({
|
||||
...transformPkiCollectionItem(p),
|
||||
notBefore: p.notBefore,
|
||||
notAfter: p.notAfter,
|
||||
friendlyName: p.friendlyName
|
||||
})),
|
||||
totalCount: count
|
||||
};
|
||||
};
|
||||
|
||||
@@ -22,8 +22,14 @@ export enum PkiItemType {
|
||||
CA = "ca"
|
||||
}
|
||||
|
||||
export const pkiItemTypeToNameMap: { [K in PkiItemType]: string } = {
|
||||
[PkiItemType.CA]: "CA",
|
||||
[PkiItemType.CERTIFICATE]: "Certificate"
|
||||
};
|
||||
|
||||
export type TGetPkiCollectionItems = {
|
||||
collectionId: string;
|
||||
type?: PkiItemType;
|
||||
offset: number;
|
||||
limit: number;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TAuditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
|
||||
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
|
||||
|
||||
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
||||
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
||||
@@ -18,6 +19,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
||||
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
|
||||
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">;
|
||||
queueService: TQueueServiceFactory;
|
||||
pkiAlertService: Pick<TPkiAlertServiceFactory, "sendPkiItemExpiryNotices">;
|
||||
};
|
||||
|
||||
export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyResourceCleanUpQueueServiceFactory>;
|
||||
@@ -25,6 +27,7 @@ export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyRe
|
||||
export const dailyResourceCleanUpQueueServiceFactory = ({
|
||||
auditLogDAL,
|
||||
queueService,
|
||||
pkiAlertService,
|
||||
snapshotDAL,
|
||||
secretVersionDAL,
|
||||
secretFolderVersionDAL,
|
||||
@@ -41,6 +44,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
||||
await secretVersionDAL.pruneExcessVersions();
|
||||
await secretVersionV2DAL.pruneExcessVersions();
|
||||
await secretFolderVersionDAL.pruneExcessVersions();
|
||||
await pkiAlertService.sendPkiItemExpiryNotices();
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
|
||||
});
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@ export enum SmtpTemplates {
|
||||
ResetPassword = "passwordReset.handlebars",
|
||||
SecretLeakIncident = "secretLeakIncident.handlebars",
|
||||
WorkspaceInvite = "workspaceInvitation.handlebars",
|
||||
ScimUserProvisioned = "scimUserProvisioned.handlebars"
|
||||
ScimUserProvisioned = "scimUserProvisioned.handlebars",
|
||||
PkiExpirationAlert = "pkiExpirationAlert.handlebars"
|
||||
}
|
||||
|
||||
export enum SmtpHost {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Infisical CA/Certificate expiration notice</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Hello,</p>
|
||||
<p>This is an automated alert for "{{alertName}}" triggered for CAs/Certificates expiring in
|
||||
{{alertBeforeDays}}
|
||||
days.</p>
|
||||
|
||||
<p>Expiring Items:</p>
|
||||
<ul>
|
||||
{{#each items}}
|
||||
<li>
|
||||
{{type}}:
|
||||
<strong>{{friendlyName}}</strong>
|
||||
<br />Serial Number:
|
||||
{{serialNumber}}
|
||||
<br />Expires On:
|
||||
{{expiryDate}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
||||
<p>Please take necessary actions to renew these items before they expire.</p>
|
||||
|
||||
<p>For more details, please log in to your Infisical account and check your PKI management section.</p>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user