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

View File

@@ -38,6 +38,7 @@ export async function up(knex: Knex): Promise<void> {
t.string("name").notNullable();
t.integer("alertBeforeDays").notNullable();
t.string("recipientEmails").notNullable();
t.unique(["name", "projectId"]);
});
}

View File

@@ -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.",

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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
};
};

View File

@@ -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,

View File

@@ -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
};
};

View File

@@ -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
};
};

View File

@@ -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">;

View File

@@ -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`);
});

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 {

View File

@@ -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>

View File

@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/pki/alerts"
---

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/pki/alerts/{alertId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Retrieve"
openapi: "GET /api/v1/pki/alerts/{alertId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/pki/alerts/{alertId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Add Collection Item"
openapi: "POST /api/v1/pki/collections/{collectionId}/items"
---

View File

@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/pki/collections"
---

View File

@@ -0,0 +1,4 @@
---
title: "Delete Collection Item"
openapi: "DELETE /api/v1/pki/collections/{collectionId}/items/{collectionItemId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/pki/collections/{collectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Retrieve"
openapi: "GET /api/v1/pki/collections/{collectionId}/items"
---

View File

@@ -0,0 +1,4 @@
---
title: "Retrieve"
openapi: "GET /api/v1/pki/collections/{collectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/pki/collections/{collectionId}"
---

View File

@@ -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
<Tabs>
<Tab title="Infisical UI">
<Steps>
<Step title="Creating a PKI/Certificate collection">
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)
</Step>
<Step title="Creating an alert">
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.
</Step>
</Steps>
</Tab>
<Tab title="API">
<Steps>
<Step title="Creating a PKI/Certificate collection">
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 <access-token>' \
--header 'Content-Type: application/json' \
--data-raw '{
"projectId": "<your-project-id>",
"name": "My Certificate Collection"
}'
```
### Sample response
```bash Response
{
id: "<collection-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/<collection-id>/items' \
--header 'Authorization: Bearer <access-token>' \
--header 'Content-Type: application/json' \
--data-raw '{
"type": "certificate",
"itemId": "id-of-certificate"
}'
```
### Sample response
```bash Response
{
id: "<collection-item-id>",
type: "certificate",
itemId: "id-of-certificate"
...
}
```
</Step>
<Step title="Creating an alert">
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 <access-token>' \
--header 'Content-Type: application/json' \
--data-raw '{
"projectId": "<your-project-id>",
"pkiCollectionId": "<your-collection-id>",
"name": "My Alert",
"alertBeforeDays": 30,
"emails": ["johndoe@gmail.com", "janedoe@gmail.com"]
}'
```
### Sample response
```bash Response
{
id: "<alert-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.
</Step>
</Steps>
</Tab>
</Tabs>

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 KiB

View File

@@ -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"
]
}
]
},

View File

@@ -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

View File

@@ -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 } }) => (
<FormControl
label="PKI Collection"
label="Certificate Collection"
errorText={error?.message}
isError={Boolean(error)}
isRequired
@@ -262,16 +261,12 @@ export const PkiAlertModal = ({ popUp, handlePopUpToggle }: Props) => {
name="emails"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Recipient Email(s)"
label="Emails to Alert"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<TextArea
{...field}
placeholder="aturing@gmail.com, alovelace@gmail.com, ..."
reSize="none"
/>
<Input {...field} placeholder="johndoe@gmail.com, janedoe@gmail.com, ..." />
</FormControl>
)}
/>

View File

@@ -17,7 +17,8 @@ import {
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { useDeletePkiCollection,useGetPkiCollectionById } from "@app/hooks/api";
import { useDeletePkiCollection, useGetPkiCollectionById } from "@app/hooks/api";
import { PkiItemType } from "@app/hooks/api/pkiCollections/constants";
import { usePopUp } from "@app/hooks/usePopUp";
import { PkiCollectionModal } from "../CertificatesPage/components/PkiAlertsTab/components/PkiCollectionModal";
@@ -137,7 +138,15 @@ export const PkiCollectionPage = withProjectPermission(
handlePopUpOpen={handlePopUpOpen}
/>
</div>
<PkiCollectionItemsSection collectionId={collectionId} />
<div className="w-full">
<div className="mb-4">
<PkiCollectionItemsSection collectionId={collectionId} type={PkiItemType.CA} />
</div>
<PkiCollectionItemsSection
collectionId={collectionId}
type={PkiItemType.CERTIFICATE}
/>
</div>
</div>
</div>
)}

View File

@@ -9,13 +9,14 @@ import {
CaStatus,
useAddItemToPkiCollection,
useListWorkspaceCas,
useListWorkspaceCertificates} from "@app/hooks/api";
useListWorkspaceCertificates
} from "@app/hooks/api";
import { PkiItemType, pkiItemTypeToNameMap } from "@app/hooks/api/pkiCollections/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z
.object({
type: z.nativeEnum(PkiItemType),
// type: z.nativeEnum(PkiItemType),
itemId: z.string()
})
.required();
@@ -24,6 +25,7 @@ type FormData = z.infer<typeof schema>;
type Props = {
collectionId: string;
type: PkiItemType;
popUp: UsePopUpState<["addPkiCollectionItem"]>;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["addPkiCollectionItem"]>,
@@ -33,7 +35,12 @@ type Props = {
// note: this component should be optimized so it is easier
// to find certificates and CAs
export const AddPkiCollectionItemModal = ({ collectionId, popUp, handlePopUpToggle }: Props) => {
export const AddPkiCollectionItemModal = ({
collectionId,
type,
popUp,
handlePopUpToggle
}: Props) => {
const { currentWorkspace } = useWorkspace();
const { data: cas } = useListWorkspaceCas({
@@ -53,18 +60,15 @@ export const AddPkiCollectionItemModal = ({ collectionId, popUp, handlePopUpTogg
control,
handleSubmit,
reset,
formState: { isSubmitting },
watch
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
type: PkiItemType.CA
}
resolver: zodResolver(schema)
// defaultValues: {
// type: PkiItemType.CA
// }
});
const itemType = watch("type");
const onFormSubmit = async ({ type, itemId }: FormData) => {
const onFormSubmit = async ({ itemId }: FormData) => {
try {
const item = await addItemToPkiCollection({
collectionId,
@@ -94,9 +98,9 @@ export const AddPkiCollectionItemModal = ({ collectionId, popUp, handlePopUpTogg
reset();
}}
>
<ModalContent title="Add CA / Certificate to Collection">
<ModalContent title={`Add ${pkiItemTypeToNameMap[type]} to Collection`}>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
{/* <Controller
control={control}
name="type"
defaultValue={PkiItemType.CA}
@@ -117,18 +121,18 @@ export const AddPkiCollectionItemModal = ({ collectionId, popUp, handlePopUpTogg
</Select>
</FormControl>
)}
/>
{itemType === PkiItemType.CA && (
/> */}
{type === PkiItemType.CA && (
<Controller
control={control}
name="itemId"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="CA"
label={pkiItemTypeToNameMap[type]}
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
// className="mt-4"
>
<Select
defaultValue={field.value}
@@ -146,7 +150,7 @@ export const AddPkiCollectionItemModal = ({ collectionId, popUp, handlePopUpTogg
)}
/>
)}
{itemType === PkiItemType.CERTIFICATE && (
{type === PkiItemType.CERTIFICATE && (
<Controller
control={control}
name="itemId"
@@ -156,7 +160,7 @@ export const AddPkiCollectionItemModal = ({ collectionId, popUp, handlePopUpTogg
label="Certificate"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
// className="mt-4"
>
<Select
defaultValue={field.value}

View File

@@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal, IconButton } from "@app/components/v2";
import { useGetPkiCollectionById, useRemoveItemFromPkiCollection } from "@app/hooks/api";
import { PkiItemType } from "@app/hooks/api/pkiCollections/constants";
import { usePopUp } from "@app/hooks/usePopUp";
import { AddPkiCollectionItemModal } from "./AddPkiCollectionItemModal";
@@ -11,9 +12,10 @@ import { PkiCollectionItemsTable } from "./PkiCollectionItemsTable";
type Props = {
collectionId: string;
type: PkiItemType;
};
export const PkiCollectionItemsSection = ({ collectionId }: Props) => {
export const PkiCollectionItemsSection = ({ collectionId, type }: Props) => {
const { data: pkiCollection } = useGetPkiCollectionById(collectionId);
const { mutateAsync: removeItemFromPkiCollection } = useRemoveItemFromPkiCollection();
@@ -40,10 +42,12 @@ export const PkiCollectionItemsSection = ({ collectionId }: Props) => {
}
};
const sectionName = type === PkiItemType.CA ? "Certificate Authorities" : "Certificates";
return pkiCollection ? (
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Items</h3>
<h3 className="text-lg font-semibold text-mineshaft-100">{sectionName}</h3>
<IconButton
ariaLabel="copy icon"
variant="plain"
@@ -56,10 +60,15 @@ export const PkiCollectionItemsSection = ({ collectionId }: Props) => {
</IconButton>
</div>
<div className="py-4">
<PkiCollectionItemsTable collectionId={collectionId} handlePopUpOpen={handlePopUpOpen} />
<PkiCollectionItemsTable
type={type}
collectionId={collectionId}
handlePopUpOpen={handlePopUpOpen}
/>
</div>
<AddPkiCollectionItemModal
collectionId={collectionId}
type={type}
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
/>

View File

@@ -1,6 +1,7 @@
import { useState } from "react";
import { faBoxesStacked, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
@@ -15,27 +16,30 @@ import {
Th,
THead,
Tooltip,
Tr} from "@app/components/v2";
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useListPkiCollectionItems } from "@app/hooks/api";
import { PkiItemType,pkiItemTypeToNameMap } from "@app/hooks/api/pkiCollections/constants";
import { PkiItemType, pkiItemTypeToNameMap } from "@app/hooks/api/pkiCollections/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
collectionId: string;
type: PkiItemType;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["deletePkiCollectionItem"]>, data?: {}) => void;
};
const PER_PAGE_INIT = 25;
export const PkiCollectionItemsTable = ({ collectionId, handlePopUpOpen }: Props) => {
export const PkiCollectionItemsTable = ({ collectionId, type, handlePopUpOpen }: Props) => {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(PER_PAGE_INIT);
const { data, isLoading } = useListPkiCollectionItems({
collectionId,
offset: (page - 1) * perPage,
limit: perPage
limit: perPage,
type
});
return (
@@ -44,8 +48,9 @@ export const PkiCollectionItemsTable = ({ collectionId, handlePopUpOpen }: Props
<Table>
<THead>
<Tr>
<Th>Resource</Th>
<Th>ID</Th>
<Th>Friendly Name</Th>
<Th>Not Before</Th>
<Th>Not After</Th>
<Th className="w-5" />
</Tr>
</THead>
@@ -55,8 +60,9 @@ export const PkiCollectionItemsTable = ({ collectionId, handlePopUpOpen }: Props
data?.collectionItems.map((collectionItem) => {
return (
<Tr className="group" key={`pki-collection-item-${collectionItem.id}`}>
<Td>{pkiItemTypeToNameMap[collectionItem.type as PkiItemType]}</Td>
<Td>{collectionItem.itemId}</Td>
<Td className="w-1/3">{collectionItem.friendlyName}</Td>
<Td>{format(new Date(collectionItem.notBefore), "yyyy-MM-dd")}</Td>
<Td>{format(new Date(collectionItem.notAfter), "yyyy-MM-dd")}</Td>
<Td>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<ProjectPermissionCan
@@ -102,7 +108,7 @@ export const PkiCollectionItemsTable = ({ collectionId, handlePopUpOpen }: Props
)}
{!isLoading && !data?.collectionItems?.length && (
<EmptyState
title="No CAs or certificates have been added to this collection"
title={`No ${pkiItemTypeToNameMap[type]}s have been added to this collection`}
icon={faBoxesStacked}
/>
)}