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

|
||||
|
||||
Give the collection a name and proceed to create the empty collection.
|
||||
|
||||

|
||||
|
||||
Next, in the Collection Page, add the certificate authorities and leaf certificates
|
||||
that you wish to monitor for expiration to the collection.
|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Creating an alert">
|
||||
To create an alert, head to your Project > Internal PKI > Alerting > Alerts and press **Create**.
|
||||
|
||||

|
||||
|
||||
Here, set the **Certificate Collection** to the PKI/Certificate collection you created in the previous step and fill out details for the alert.
|
||||
|
||||

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

|
||||
|
||||
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>
|
||||
BIN
docs/images/platform/pki/alerting/alert-create-2.png
Normal file
BIN
docs/images/platform/pki/alerting/alert-create-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 366 KiB |
BIN
docs/images/platform/pki/alerting/alert-create.png
Normal file
BIN
docs/images/platform/pki/alerting/alert-create.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 692 KiB |
BIN
docs/images/platform/pki/alerting/alerts.png
Normal file
BIN
docs/images/platform/pki/alerting/alerts.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 487 KiB |
BIN
docs/images/platform/pki/alerting/collection-add-cert.png
Normal file
BIN
docs/images/platform/pki/alerting/collection-add-cert.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 747 KiB |
BIN
docs/images/platform/pki/alerting/collection-create-2.png
Normal file
BIN
docs/images/platform/pki/alerting/collection-create-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 338 KiB |
BIN
docs/images/platform/pki/alerting/collection-create.png
Normal file
BIN
docs/images/platform/pki/alerting/collection-create.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 699 KiB |
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user