diff --git a/backend/src/db/migrations/20240806173521_cert-alerting.ts b/backend/src/db/migrations/20240806173521_cert-alerting.ts index a04955dc0..a946eb5ce 100644 --- a/backend/src/db/migrations/20240806173521_cert-alerting.ts +++ b/backend/src/db/migrations/20240806173521_cert-alerting.ts @@ -38,6 +38,7 @@ export async function up(knex: Knex): Promise { t.string("name").notNullable(); t.integer("alertBeforeDays").notNullable(); t.string("recipientEmails").notNullable(); + t.unique(["name", "projectId"]); }); } diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index bca04a758..336776dbf 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1144,6 +1144,63 @@ export const CERTIFICATES = { } }; +export const ALERTS = { + CREATE: { + projectId: "The ID of the project to create the alert in", + pkiCollectionId: "The ID of the PKI collection to bind to the alert", + name: "The name of the alert", + alertBeforeDays: "The number of days before the certificate expires to trigger the alert", + emails: "The email addresses to send the alert email to" + }, + GET: { + alertId: "The ID of the alert to get" + }, + UPDATE: { + alertId: "The ID of the alert to update", + name: "The name of the alert to update to", + alertBeforeDays: "The number of days before the certificate expires to trigger the alert to update to", + pkiCollectionId: "The ID of the PKI collection to bind to the alert to update to", + emails: "The email addresses to send the alert email to update to" + }, + DELETE: { + alertId: "The ID of the alert to delete" + } +}; + +export const PKI_COLLECTIONS = { + CREATE: { + projectId: "The ID of the project to create the PKI collection in", + name: "The name of the PKI collection" + }, + GET: { + collectionId: "The ID of the PKI collection to get" + }, + UPDATE: { + collectionId: "The ID of the PKI collection to update", + name: "The name of the PKI collection to update to" + }, + DELETE: { + collectionId: "The ID of the PKI collection to delete" + }, + LIST_ITEMS: { + collectionId: "The ID of the PKI collection to list items from", + type: "The type of the PKI collection item to list", + offset: "The offset to start from", + limit: "The number of items to return" + }, + ADD_ITEM: { + collectionId: "The ID of the PKI collection to add the item to", + type: "The type of the PKI collection item to add", + itemId: "The resource ID of the PKI collection item to add" + }, + DELETE_ITEM: { + collectionId: "The ID of the PKI collection to delete the item from", + collectionItemId: "The ID of the PKI collection item to delete", + type: "The type of the deleted PKI collection item", + itemId: "The resource ID of the deleted PKI collection item" + } +}; + export const PROJECT_ROLE = { CREATE: { projectSlug: "Slug of the project to create the role for.", diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 279df0bd1..dbd223d7b 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -639,7 +639,8 @@ export const registerRoutes = async ( const pkiAlertService = pkiAlertServiceFactory({ pkiAlertDAL, pkiCollectionDAL, - permissionService + permissionService, + smtpService }); const pkiCollectionService = pkiCollectionServiceFactory({ @@ -1049,6 +1050,7 @@ export const registerRoutes = async ( const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ auditLogDAL, queueService, + pkiAlertService, secretVersionDAL, secretFolderVersionDAL: folderVersionDAL, snapshotDAL, diff --git a/backend/src/server/routes/v1/pki-alert-router.ts b/backend/src/server/routes/v1/pki-alert-router.ts index 34258e4c0..afaa99bdf 100644 --- a/backend/src/server/routes/v1/pki-alert-router.ts +++ b/backend/src/server/routes/v1/pki-alert-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { PkiAlertsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ALERTS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -17,11 +18,11 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { schema: { description: "Create PKI alert", body: z.object({ - projectId: z.string().trim(), - pkiCollectionId: z.string().trim(), - name: z.string().trim(), - alertBeforeDays: z.number(), - emails: z.array(z.string()) + projectId: z.string().trim().describe(ALERTS.CREATE.projectId), + pkiCollectionId: z.string().trim().describe(ALERTS.CREATE.pkiCollectionId), + name: z.string().trim().describe(ALERTS.CREATE.name), + alertBeforeDays: z.number().describe(ALERTS.CREATE.alertBeforeDays), + emails: z.array(z.string().trim().email({ message: "Invalid email address" })).describe(ALERTS.CREATE.emails) }), response: { 200: PkiAlertsSchema @@ -65,7 +66,7 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { schema: { description: "Get PKI alert", params: z.object({ - alertId: z.string().trim() + alertId: z.string().trim().describe(ALERTS.GET.alertId) }), response: { 200: PkiAlertsSchema @@ -105,13 +106,16 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { schema: { description: "Update PKI alert", params: z.object({ - alertId: z.string().trim() + alertId: z.string().trim().describe(ALERTS.UPDATE.alertId) }), body: z.object({ - name: z.string().trim().optional(), - alertBeforeDays: z.number().optional(), - pkiCollectionId: z.string().trim().optional(), - emails: z.array(z.string()).optional() + name: z.string().trim().optional().describe(ALERTS.UPDATE.name), + alertBeforeDays: z.number().optional().describe(ALERTS.UPDATE.alertBeforeDays), + pkiCollectionId: z.string().trim().optional().describe(ALERTS.UPDATE.pkiCollectionId), + emails: z + .array(z.string().trim().email({ message: "Invalid email address" })) + .optional() + .describe(ALERTS.UPDATE.emails) }), response: { 200: PkiAlertsSchema @@ -156,7 +160,7 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { schema: { description: "Delete PKI alert", params: z.object({ - alertId: z.string().trim() + alertId: z.string().trim().describe(ALERTS.DELETE.alertId) }), response: { 200: PkiAlertsSchema diff --git a/backend/src/server/routes/v1/pki-collection-router.ts b/backend/src/server/routes/v1/pki-collection-router.ts index 2188ad8cb..6a48b5e76 100644 --- a/backend/src/server/routes/v1/pki-collection-router.ts +++ b/backend/src/server/routes/v1/pki-collection-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { PkiCollectionItemsSchema, PkiCollectionsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { PKI_COLLECTIONS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -18,8 +19,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => schema: { description: "Create PKI collection", body: z.object({ - projectId: z.string().trim(), - name: z.string().trim() + projectId: z.string().trim().describe(PKI_COLLECTIONS.CREATE.projectId), + name: z.string().trim().describe(PKI_COLLECTIONS.CREATE.name) }), response: { 200: PkiCollectionsSchema @@ -60,7 +61,7 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => schema: { description: "Get PKI collection", params: z.object({ - collectionId: z.string().trim() + collectionId: z.string().trim().describe(PKI_COLLECTIONS.GET.collectionId) }), response: { 200: PkiCollectionsSchema @@ -100,10 +101,10 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => schema: { description: "Update PKI collection", params: z.object({ - collectionId: z.string().trim() + collectionId: z.string().trim().describe(PKI_COLLECTIONS.UPDATE.collectionId) }), body: z.object({ - name: z.string().trim().optional() + name: z.string().trim().optional().describe(PKI_COLLECTIONS.UPDATE.name) }), response: { 200: PkiCollectionsSchema @@ -145,7 +146,7 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => schema: { description: "Delete PKI collection", params: z.object({ - collectionId: z.string().trim() + collectionId: z.string().trim().describe(PKI_COLLECTIONS.DELETE.collectionId) }), response: { 200: PkiCollectionsSchema @@ -185,18 +186,22 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => schema: { description: "Get items in PKI collection", params: z.object({ - collectionId: z.string().trim() + collectionId: z.string().trim().describe(PKI_COLLECTIONS.LIST_ITEMS.collectionId) }), querystring: z.object({ - offset: z.coerce.number().min(0).max(100).default(0), - limit: z.coerce.number().min(1).max(100).default(25) + type: z.nativeEnum(PkiItemType).optional().describe(PKI_COLLECTIONS.LIST_ITEMS.type), + offset: z.coerce.number().min(0).max(100).default(0).describe(PKI_COLLECTIONS.LIST_ITEMS.offset), + limit: z.coerce.number().min(1).max(100).default(25).describe(PKI_COLLECTIONS.LIST_ITEMS.limit) }), response: { 200: z.object({ collectionItems: z.array( PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({ type: z.nativeEnum(PkiItemType), - itemId: z.string().trim() + itemId: z.string().trim(), + notBefore: z.date(), + notAfter: z.date(), + friendlyName: z.string().trim() }) ), totalCount: z.number() @@ -242,16 +247,16 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => schema: { description: "Add item to PKI collection", params: z.object({ - collectionId: z.string().trim() + collectionId: z.string().trim().describe(PKI_COLLECTIONS.ADD_ITEM.collectionId) }), body: z.object({ - type: z.nativeEnum(PkiItemType), - itemId: z.string().trim() + type: z.nativeEnum(PkiItemType).describe(PKI_COLLECTIONS.ADD_ITEM.type), + itemId: z.string().trim().describe(PKI_COLLECTIONS.ADD_ITEM.itemId) }), response: { 200: PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({ - type: z.nativeEnum(PkiItemType), - itemId: z.string().trim() + type: z.nativeEnum(PkiItemType).describe(PKI_COLLECTIONS.ADD_ITEM.type), + itemId: z.string().trim().describe(PKI_COLLECTIONS.ADD_ITEM.itemId) }) } }, @@ -285,7 +290,7 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => server.route({ method: "DELETE", - url: "/:collectionId/items/:itemId", + url: "/:collectionId/items/:collectionItemId", config: { rateLimit: writeLimit }, @@ -293,20 +298,20 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => schema: { description: "Remove item from PKI collection", params: z.object({ - collectionId: z.string().trim(), - itemId: z.string().trim() + collectionId: z.string().trim().describe(PKI_COLLECTIONS.DELETE_ITEM.collectionId), + collectionItemId: z.string().trim().describe(PKI_COLLECTIONS.DELETE_ITEM.collectionItemId) }), response: { 200: PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({ - type: z.nativeEnum(PkiItemType), - itemId: z.string().trim() + type: z.nativeEnum(PkiItemType).describe(PKI_COLLECTIONS.DELETE_ITEM.type), + itemId: z.string().trim().describe(PKI_COLLECTIONS.DELETE_ITEM.itemId) }) } }, handler: async (req) => { const { pkiCollection, pkiCollectionItem } = await server.services.pkiCollection.removeItemFromPkiCollection({ collectionId: req.params.collectionId, - itemId: req.params.itemId, + itemId: req.params.collectionItemId, actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, diff --git a/backend/src/services/pki-alert/pki-alert-dal.ts b/backend/src/services/pki-alert/pki-alert-dal.ts index 7e12813e1..d4d4fa987 100644 --- a/backend/src/services/pki-alert/pki-alert-dal.ts +++ b/backend/src/services/pki-alert/pki-alert-dal.ts @@ -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; 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 }; }; diff --git a/backend/src/services/pki-alert/pki-alert-service.ts b/backend/src/services/pki-alert/pki-alert-service.ts index 3692afe4d..44f029d42 100644 --- a/backend/src/services/pki-alert/pki-alert-service.ts +++ b/backend/src/services/pki-alert/pki-alert-service.ts @@ -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; + smtpService: Pick; }; export type TPkiAlertServiceFactory = ReturnType; @@ -19,8 +23,42 @@ export type TPkiAlertServiceFactory = ReturnType; 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, diff --git a/backend/src/services/pki-collection/pki-collection-item-dal.ts b/backend/src/services/pki-collection/pki-collection-item-dal.ts index 57763aefb..403fd928b 100644 --- a/backend/src/services/pki-collection/pki-collection-item-dal.ts +++ b/backend/src/services/pki-collection/pki-collection-item-dal.ts @@ -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; 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 }; }; diff --git a/backend/src/services/pki-collection/pki-collection-service.ts b/backend/src/services/pki-collection/pki-collection-service.ts index 4be4099f9..87b1be2f9 100644 --- a/backend/src/services/pki-collection/pki-collection-service.ts +++ b/backend/src/services/pki-collection/pki-collection-service.ts @@ -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 }; }; diff --git a/backend/src/services/pki-collection/pki-collection-types.ts b/backend/src/services/pki-collection/pki-collection-types.ts index ec7ce3cfa..52d489eb2 100644 --- a/backend/src/services/pki-collection/pki-collection-types.ts +++ b/backend/src/services/pki-collection/pki-collection-types.ts @@ -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; diff --git a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts index cb9756dd4..83725f536 100644 --- a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts +++ b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts @@ -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; secretSharingDAL: Pick; queueService: TQueueServiceFactory; + pkiAlertService: Pick; }; export type TDailyResourceCleanUpQueueServiceFactory = ReturnType; @@ -25,6 +27,7 @@ export type TDailyResourceCleanUpQueueServiceFactory = ReturnType + + + + Infisical CA/Certificate expiration notice + + +

Hello,

+

This is an automated alert for "{{alertName}}" triggered for CAs/Certificates expiring in + {{alertBeforeDays}} + days.

+ +

Expiring Items:

+
    + {{#each items}} +
  • + {{type}}: + {{friendlyName}} +
    Serial Number: + {{serialNumber}} +
    Expires On: + {{expiryDate}} +
  • + {{/each}} +
+ +

Please take necessary actions to renew these items before they expire.

+ +

For more details, please log in to your Infisical account and check your PKI management section.

+ + \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki-alerts/create.mdx b/docs/api-reference/endpoints/pki-alerts/create.mdx new file mode 100644 index 000000000..458f0cd48 --- /dev/null +++ b/docs/api-reference/endpoints/pki-alerts/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/pki/alerts" +--- diff --git a/docs/api-reference/endpoints/pki-alerts/delete.mdx b/docs/api-reference/endpoints/pki-alerts/delete.mdx new file mode 100644 index 000000000..c0918d1fe --- /dev/null +++ b/docs/api-reference/endpoints/pki-alerts/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/pki/alerts/{alertId}" +--- diff --git a/docs/api-reference/endpoints/pki-alerts/read.mdx b/docs/api-reference/endpoints/pki-alerts/read.mdx new file mode 100644 index 000000000..928afdbc5 --- /dev/null +++ b/docs/api-reference/endpoints/pki-alerts/read.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve" +openapi: "GET /api/v1/pki/alerts/{alertId}" +--- diff --git a/docs/api-reference/endpoints/pki-alerts/update.mdx b/docs/api-reference/endpoints/pki-alerts/update.mdx new file mode 100644 index 000000000..829f8c57b --- /dev/null +++ b/docs/api-reference/endpoints/pki-alerts/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/pki/alerts/{alertId}" +--- diff --git a/docs/api-reference/endpoints/pki-collections/add-item.mdx b/docs/api-reference/endpoints/pki-collections/add-item.mdx new file mode 100644 index 000000000..7a7da9c2e --- /dev/null +++ b/docs/api-reference/endpoints/pki-collections/add-item.mdx @@ -0,0 +1,4 @@ +--- +title: "Add Collection Item" +openapi: "POST /api/v1/pki/collections/{collectionId}/items" +--- diff --git a/docs/api-reference/endpoints/pki-collections/create.mdx b/docs/api-reference/endpoints/pki-collections/create.mdx new file mode 100644 index 000000000..7211b622c --- /dev/null +++ b/docs/api-reference/endpoints/pki-collections/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/pki/collections" +--- diff --git a/docs/api-reference/endpoints/pki-collections/delete-item.mdx b/docs/api-reference/endpoints/pki-collections/delete-item.mdx new file mode 100644 index 000000000..e5805618b --- /dev/null +++ b/docs/api-reference/endpoints/pki-collections/delete-item.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete Collection Item" +openapi: "DELETE /api/v1/pki/collections/{collectionId}/items/{collectionItemId}" +--- diff --git a/docs/api-reference/endpoints/pki-collections/delete.mdx b/docs/api-reference/endpoints/pki-collections/delete.mdx new file mode 100644 index 000000000..46e67bc4f --- /dev/null +++ b/docs/api-reference/endpoints/pki-collections/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/pki/collections/{collectionId}" +--- diff --git a/docs/api-reference/endpoints/pki-collections/list-items.mdx b/docs/api-reference/endpoints/pki-collections/list-items.mdx new file mode 100644 index 000000000..dc2e1f04b --- /dev/null +++ b/docs/api-reference/endpoints/pki-collections/list-items.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve" +openapi: "GET /api/v1/pki/collections/{collectionId}/items" +--- diff --git a/docs/api-reference/endpoints/pki-collections/read.mdx b/docs/api-reference/endpoints/pki-collections/read.mdx new file mode 100644 index 000000000..de83f7996 --- /dev/null +++ b/docs/api-reference/endpoints/pki-collections/read.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve" +openapi: "GET /api/v1/pki/collections/{collectionId}" +--- diff --git a/docs/api-reference/endpoints/pki-collections/update.mdx b/docs/api-reference/endpoints/pki-collections/update.mdx new file mode 100644 index 000000000..7d24214e0 --- /dev/null +++ b/docs/api-reference/endpoints/pki-collections/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/pki/collections/{collectionId}" +--- diff --git a/docs/documentation/platform/pki/alerting.mdx b/docs/documentation/platform/pki/alerting.mdx new file mode 100644 index 000000000..158c47877 --- /dev/null +++ b/docs/documentation/platform/pki/alerting.mdx @@ -0,0 +1,149 @@ +--- +title: "Alerting" +description: "Learn how to set up alerting for expiring certificates with Infisical" +--- + +## Concept + +In order to ensure that your certificates are always up-to-date and not expired, you can set up alerting for expiring CA and leaf certificates in Infisical. + +## Workflow + +A typical alerting workflow for expiring certificates consists of the following steps: + +1. Creating a PKI/Certificate collection and adding certificates that you wish to monitor for expiration to it. +2. Creating an alert and binding it to the PKI/Certificate collection. As part of the configuration, you specify when the alert should trigger based on the number of days before certificate expiration and the email addresses of the recipients to notify. + +## Guide to Creating an Alert + + + + + + To create a PKI/Certificate collection, head to your Project > Internal + PKI > Alerting > Certificate Collection and press **Create**. + + ![pki create collection](/images/platform/pki/alerting/collection-create.png) + + Give the collection a name and proceed to create the empty collection. + + ![pki create collection](/images/platform/pki/alerting/collection-create-2.png) + + Next, in the Collection Page, add the certificate authorities and leaf certificates + that you wish to monitor for expiration to the collection. + + ![pki add cert to collection](/images/platform/pki/alerting/collection-add-cert.png) + + + To create an alert, head to your Project > Internal PKI > Alerting > Alerts and press **Create**. + + ![pki create alert](/images/platform/pki/alerting/alert-create.png) + + Here, set the **Certificate Collection** to the PKI/Certificate collection you created in the previous step and fill out details for the alert. + + ![pki create alert](/images/platform/pki/alerting/alert-create-2.png) + + Here's some guidance on each field: + + - Name: A name for the alert. + - Collection Collection: The PKI/Certificate collection to bind the alert to from the previous step. + - Alert Before / Unit: The time before certificate expiration to trigger the alert. + - Emails to Alert: A comma-delimited list of email addresses to notify when the alert triggers. + + Finally, press **Create** to create the alert. + + ![pki alerts](/images/platform/pki/alerting/alerts.png) + + Great! You've successfully created a PKI/Certificate collection and an alert to monitor the expiring certificates in the collection. Once the alert triggers, the specified email addresses will be notified. + + + + + + + + 1.1. To create a PKI/Certificate collection, make an API request to the [Create PKI Collection](/api-reference/endpoints/pki-collections/create) API endpoint. + + ### Sample request + + ```bash Request + curl --location --request POST 'https://app.infisical.com/api/v1/pki/collections' \ + --header 'Authorization: Bearer ' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "projectId": "", + "name": "My Certificate Collection" + }' + ``` + + ### Sample response + + ```bash Response + { + id: "", + name: "My Certificate Collection", + ... + } + ``` + + 1.2. Next, make an API request to the [Add Collection Item](/api-reference/endpoints/pki-collections/add-item) API endpoint to add a certificate to the collection. + + ### Sample request + + ```bash Request + curl --location --request POST 'https://app.infisical.com/api/v1/pki/collections//items' \ + --header 'Authorization: Bearer ' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "type": "certificate", + "itemId": "id-of-certificate" + }' + ``` + + ### Sample response + + ```bash Response + { + id: "", + type: "certificate", + itemId: "id-of-certificate" + ... + } + ``` + + + To create an alert, make an API request to the [Create Alert](/api-reference/endpoints/pki-alerts/create) API endpoint, specifying the PKI/Certificate collection to bind the alert to, the alert configuration, and the email addresses to notify. + + ### Sample request + + ```bash Request + curl --location --request POST 'https://app.infisical.com/api/v1/pki/alerts' \ + --header 'Authorization: Bearer ' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "projectId": "", + "pkiCollectionId": "", + "name": "My Alert", + "alertBeforeDays": 30, + "emails": ["johndoe@gmail.com", "janedoe@gmail.com"] + }' + ``` + + ### Sample response + + ```bash Response + { + id: "", + name: "My Alert", + alertBeforeDays: 30, + recipientEmails: "johndoe@gmail.com,janedoe@gmail.com" + ... + } + ``` + + Great! You've successfully created a PKI/Certificate collection and an alert to monitor the expiring certificate in the collection. Once the alert triggers, the specified email addresses will be notified. + + + + + diff --git a/docs/images/platform/pki/alerting/alert-create-2.png b/docs/images/platform/pki/alerting/alert-create-2.png new file mode 100644 index 000000000..812f253f8 Binary files /dev/null and b/docs/images/platform/pki/alerting/alert-create-2.png differ diff --git a/docs/images/platform/pki/alerting/alert-create.png b/docs/images/platform/pki/alerting/alert-create.png new file mode 100644 index 000000000..4a7b3227f Binary files /dev/null and b/docs/images/platform/pki/alerting/alert-create.png differ diff --git a/docs/images/platform/pki/alerting/alerts.png b/docs/images/platform/pki/alerting/alerts.png new file mode 100644 index 000000000..c7a5096ce Binary files /dev/null and b/docs/images/platform/pki/alerting/alerts.png differ diff --git a/docs/images/platform/pki/alerting/collection-add-cert.png b/docs/images/platform/pki/alerting/collection-add-cert.png new file mode 100644 index 000000000..6300cc892 Binary files /dev/null and b/docs/images/platform/pki/alerting/collection-add-cert.png differ diff --git a/docs/images/platform/pki/alerting/collection-create-2.png b/docs/images/platform/pki/alerting/collection-create-2.png new file mode 100644 index 000000000..53378ef05 Binary files /dev/null and b/docs/images/platform/pki/alerting/collection-create-2.png differ diff --git a/docs/images/platform/pki/alerting/collection-create.png b/docs/images/platform/pki/alerting/collection-create.png new file mode 100644 index 000000000..7c4883201 Binary files /dev/null and b/docs/images/platform/pki/alerting/collection-create.png differ diff --git a/docs/mint.json b/docs/mint.json index 5146084fb..ac61ad5bd 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -107,7 +107,8 @@ "pages": [ "documentation/platform/pki/overview", "documentation/platform/pki/private-ca", - "documentation/platform/pki/certificates" + "documentation/platform/pki/certificates", + "documentation/platform/pki/alerting" ] }, { @@ -687,6 +688,27 @@ "api-reference/endpoints/certificates/delete", "api-reference/endpoints/certificates/cert-body" ] + }, + { + "group": "Certificate Collections", + "pages": [ + "api-reference/endpoints/pki-collections/create", + "api-reference/endpoints/pki-collections/read", + "api-reference/endpoints/pki-collections/update", + "api-reference/endpoints/pki-collections/delete", + "api-reference/endpoints/pki-collections/add-item", + "api-reference/endpoints/pki-collections/list-items", + "api-reference/endpoints/pki-collections/delete-item" + ] + }, + { + "group": "PKI Alerting", + "pages": [ + "api-reference/endpoints/pki-alerts/create", + "api-reference/endpoints/pki-alerts/read", + "api-reference/endpoints/pki-alerts/update", + "api-reference/endpoints/pki-alerts/delete" + ] } ] }, diff --git a/frontend/src/hooks/api/pkiCollections/queries.tsx b/frontend/src/hooks/api/pkiCollections/queries.tsx index c5013fcf3..109862767 100644 --- a/frontend/src/hooks/api/pkiCollections/queries.tsx +++ b/frontend/src/hooks/api/pkiCollections/queries.tsx @@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { PkiItemType } from "./constants"; import { TPkiCollection, TPkiCollectionItem } from "./types"; export const pkiCollectionKeys = { @@ -10,16 +11,18 @@ export const pkiCollectionKeys = { [{ collectionId }, "pki-collection-items"] as const, specificPkiCollectionItems: ({ collectionId, + type, offset, limit }: { collectionId: string; + type?: PkiItemType; offset: number; limit: number; }) => [ ...pkiCollectionKeys.getPkiCollectionItems(collectionId), - { offset, limit }, + { offset, limit, type }, "pki-collection-items-2" ] as const }; @@ -39,10 +42,12 @@ export const useGetPkiCollectionById = (collectionId: string) => { export const useListPkiCollectionItems = ({ collectionId, + type, offset, limit }: { collectionId: string; + type?: PkiItemType; offset: number; limit: number; }) => { @@ -50,18 +55,24 @@ export const useListPkiCollectionItems = ({ queryKey: pkiCollectionKeys.specificPkiCollectionItems({ collectionId, offset, - limit + limit, + type }), queryFn: async () => { const params = new URLSearchParams({ offset: String(offset), - limit: String(limit) + limit: String(limit), + ...(type ? { type } : {}) }); const { data: { collectionItems, totalCount } } = await apiRequest.get<{ - collectionItems: TPkiCollectionItem[]; + collectionItems: (TPkiCollectionItem & { + notBefore: string; + notAfter: string; + friendlyName: string; + })[]; totalCount: number; }>(`/api/v1/pki/collections/${collectionId}/items`, { params diff --git a/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiAlertModal.tsx b/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiAlertModal.tsx index 62ae89476..efb63bdf2 100644 --- a/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiAlertModal.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiAlertModal.tsx @@ -11,8 +11,7 @@ import { Modal, ModalContent, Select, - SelectItem, - TextArea + SelectItem } from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { @@ -193,7 +192,7 @@ export const PkiAlertModal = ({ popUp, handlePopUpToggle }: Props) => { defaultValue="" render={({ field: { onChange, ...field }, fieldState: { error } }) => ( { name="emails" render={({ field, fieldState: { error } }) => ( -