Finish docs for pki alerting + expose endpoints

This commit is contained in:
Tuan Dang
2024-08-09 19:22:47 -07:00
parent e1b9965f01
commit f4244c6d4d
38 changed files with 630 additions and 91 deletions
@@ -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`);
});
+2 -1
View File
@@ -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>