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.string("name").notNullable();
t.integer("alertBeforeDays").notNullable(); t.integer("alertBeforeDays").notNullable();
t.string("recipientEmails").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 = { export const PROJECT_ROLE = {
CREATE: { CREATE: {
projectSlug: "Slug of the project to create the role for.", projectSlug: "Slug of the project to create the role for.",

View File

@@ -639,7 +639,8 @@ export const registerRoutes = async (
const pkiAlertService = pkiAlertServiceFactory({ const pkiAlertService = pkiAlertServiceFactory({
pkiAlertDAL, pkiAlertDAL,
pkiCollectionDAL, pkiCollectionDAL,
permissionService permissionService,
smtpService
}); });
const pkiCollectionService = pkiCollectionServiceFactory({ const pkiCollectionService = pkiCollectionServiceFactory({
@@ -1049,6 +1050,7 @@ export const registerRoutes = async (
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
auditLogDAL, auditLogDAL,
queueService, queueService,
pkiAlertService,
secretVersionDAL, secretVersionDAL,
secretFolderVersionDAL: folderVersionDAL, secretFolderVersionDAL: folderVersionDAL,
snapshotDAL, snapshotDAL,

View File

@@ -2,6 +2,7 @@ import { z } from "zod";
import { PkiAlertsSchema } from "@app/db/schemas"; import { PkiAlertsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; 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 { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -17,11 +18,11 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
description: "Create PKI alert", description: "Create PKI alert",
body: z.object({ body: z.object({
projectId: z.string().trim(), projectId: z.string().trim().describe(ALERTS.CREATE.projectId),
pkiCollectionId: z.string().trim(), pkiCollectionId: z.string().trim().describe(ALERTS.CREATE.pkiCollectionId),
name: z.string().trim(), name: z.string().trim().describe(ALERTS.CREATE.name),
alertBeforeDays: z.number(), alertBeforeDays: z.number().describe(ALERTS.CREATE.alertBeforeDays),
emails: z.array(z.string()) emails: z.array(z.string().trim().email({ message: "Invalid email address" })).describe(ALERTS.CREATE.emails)
}), }),
response: { response: {
200: PkiAlertsSchema 200: PkiAlertsSchema
@@ -65,7 +66,7 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
description: "Get PKI alert", description: "Get PKI alert",
params: z.object({ params: z.object({
alertId: z.string().trim() alertId: z.string().trim().describe(ALERTS.GET.alertId)
}), }),
response: { response: {
200: PkiAlertsSchema 200: PkiAlertsSchema
@@ -105,13 +106,16 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
description: "Update PKI alert", description: "Update PKI alert",
params: z.object({ params: z.object({
alertId: z.string().trim() alertId: z.string().trim().describe(ALERTS.UPDATE.alertId)
}), }),
body: z.object({ body: z.object({
name: z.string().trim().optional(), name: z.string().trim().optional().describe(ALERTS.UPDATE.name),
alertBeforeDays: z.number().optional(), alertBeforeDays: z.number().optional().describe(ALERTS.UPDATE.alertBeforeDays),
pkiCollectionId: z.string().trim().optional(), pkiCollectionId: z.string().trim().optional().describe(ALERTS.UPDATE.pkiCollectionId),
emails: z.array(z.string()).optional() emails: z
.array(z.string().trim().email({ message: "Invalid email address" }))
.optional()
.describe(ALERTS.UPDATE.emails)
}), }),
response: { response: {
200: PkiAlertsSchema 200: PkiAlertsSchema
@@ -156,7 +160,7 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
description: "Delete PKI alert", description: "Delete PKI alert",
params: z.object({ params: z.object({
alertId: z.string().trim() alertId: z.string().trim().describe(ALERTS.DELETE.alertId)
}), }),
response: { response: {
200: PkiAlertsSchema 200: PkiAlertsSchema

View File

@@ -2,6 +2,7 @@ import { z } from "zod";
import { PkiCollectionItemsSchema, PkiCollectionsSchema } from "@app/db/schemas"; import { PkiCollectionItemsSchema, PkiCollectionsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; 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 { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -18,8 +19,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) =>
schema: { schema: {
description: "Create PKI collection", description: "Create PKI collection",
body: z.object({ body: z.object({
projectId: z.string().trim(), projectId: z.string().trim().describe(PKI_COLLECTIONS.CREATE.projectId),
name: z.string().trim() name: z.string().trim().describe(PKI_COLLECTIONS.CREATE.name)
}), }),
response: { response: {
200: PkiCollectionsSchema 200: PkiCollectionsSchema
@@ -60,7 +61,7 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) =>
schema: { schema: {
description: "Get PKI collection", description: "Get PKI collection",
params: z.object({ params: z.object({
collectionId: z.string().trim() collectionId: z.string().trim().describe(PKI_COLLECTIONS.GET.collectionId)
}), }),
response: { response: {
200: PkiCollectionsSchema 200: PkiCollectionsSchema
@@ -100,10 +101,10 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) =>
schema: { schema: {
description: "Update PKI collection", description: "Update PKI collection",
params: z.object({ params: z.object({
collectionId: z.string().trim() collectionId: z.string().trim().describe(PKI_COLLECTIONS.UPDATE.collectionId)
}), }),
body: z.object({ body: z.object({
name: z.string().trim().optional() name: z.string().trim().optional().describe(PKI_COLLECTIONS.UPDATE.name)
}), }),
response: { response: {
200: PkiCollectionsSchema 200: PkiCollectionsSchema
@@ -145,7 +146,7 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) =>
schema: { schema: {
description: "Delete PKI collection", description: "Delete PKI collection",
params: z.object({ params: z.object({
collectionId: z.string().trim() collectionId: z.string().trim().describe(PKI_COLLECTIONS.DELETE.collectionId)
}), }),
response: { response: {
200: PkiCollectionsSchema 200: PkiCollectionsSchema
@@ -185,18 +186,22 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) =>
schema: { schema: {
description: "Get items in PKI collection", description: "Get items in PKI collection",
params: z.object({ params: z.object({
collectionId: z.string().trim() collectionId: z.string().trim().describe(PKI_COLLECTIONS.LIST_ITEMS.collectionId)
}), }),
querystring: z.object({ querystring: z.object({
offset: z.coerce.number().min(0).max(100).default(0), type: z.nativeEnum(PkiItemType).optional().describe(PKI_COLLECTIONS.LIST_ITEMS.type),
limit: z.coerce.number().min(1).max(100).default(25) 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: { response: {
200: z.object({ 200: z.object({
collectionItems: z.array( collectionItems: z.array(
PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({ PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({
type: z.nativeEnum(PkiItemType), 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() totalCount: z.number()
@@ -242,16 +247,16 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) =>
schema: { schema: {
description: "Add item to PKI collection", description: "Add item to PKI collection",
params: z.object({ params: z.object({
collectionId: z.string().trim() collectionId: z.string().trim().describe(PKI_COLLECTIONS.ADD_ITEM.collectionId)
}), }),
body: z.object({ body: z.object({
type: z.nativeEnum(PkiItemType), type: z.nativeEnum(PkiItemType).describe(PKI_COLLECTIONS.ADD_ITEM.type),
itemId: z.string().trim() itemId: z.string().trim().describe(PKI_COLLECTIONS.ADD_ITEM.itemId)
}), }),
response: { response: {
200: PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({ 200: PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({
type: z.nativeEnum(PkiItemType), type: z.nativeEnum(PkiItemType).describe(PKI_COLLECTIONS.ADD_ITEM.type),
itemId: z.string().trim() itemId: z.string().trim().describe(PKI_COLLECTIONS.ADD_ITEM.itemId)
}) })
} }
}, },
@@ -285,7 +290,7 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) =>
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/:collectionId/items/:itemId", url: "/:collectionId/items/:collectionItemId",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
@@ -293,20 +298,20 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) =>
schema: { schema: {
description: "Remove item from PKI collection", description: "Remove item from PKI collection",
params: z.object({ params: z.object({
collectionId: z.string().trim(), collectionId: z.string().trim().describe(PKI_COLLECTIONS.DELETE_ITEM.collectionId),
itemId: z.string().trim() collectionItemId: z.string().trim().describe(PKI_COLLECTIONS.DELETE_ITEM.collectionItemId)
}), }),
response: { response: {
200: PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({ 200: PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({
type: z.nativeEnum(PkiItemType), type: z.nativeEnum(PkiItemType).describe(PKI_COLLECTIONS.DELETE_ITEM.type),
itemId: z.string().trim() itemId: z.string().trim().describe(PKI_COLLECTIONS.DELETE_ITEM.itemId)
}) })
} }
}, },
handler: async (req) => { handler: async (req) => {
const { pkiCollection, pkiCollectionItem } = await server.services.pkiCollection.removeItemFromPkiCollection({ const { pkiCollection, pkiCollectionItem } = await server.services.pkiCollection.removeItemFromPkiCollection({
collectionId: req.params.collectionId, collectionId: req.params.collectionId,
itemId: req.params.itemId, itemId: req.params.collectionItemId,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,

View File

@@ -1,12 +1,84 @@
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify } from "@app/lib/knex";
import { PkiItemType } from "../pki-collection/pki-collection-types";
export type TPkiAlertDALFactory = ReturnType<typeof pkiAlertDALFactory>; export type TPkiAlertDALFactory = ReturnType<typeof pkiAlertDALFactory>;
export const pkiAlertDALFactory = (db: TDbClient) => { export const pkiAlertDALFactory = (db: TDbClient) => {
const pkiAlertOrm = ormify(db, TableName.PkiAlert); 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 { return {
getExpiringPkiCollectionItemsForAlerting,
...pkiAlertOrm ...pkiAlertOrm
}; };
}; };

View File

@@ -3,7 +3,10 @@ import { ForbiddenError } from "@casl/ability";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal"; 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 { TPkiAlertDALFactory } from "./pki-alert-dal";
import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./pki-alert-types"; import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./pki-alert-types";
@@ -12,6 +15,7 @@ type TPkiAlertServiceFactoryDep = {
pkiAlertDAL: TPkiAlertDALFactory; pkiAlertDAL: TPkiAlertDALFactory;
pkiCollectionDAL: TPkiCollectionDALFactory; pkiCollectionDAL: TPkiCollectionDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
smtpService: Pick<TSmtpService, "sendMail">;
}; };
export type TPkiAlertServiceFactory = ReturnType<typeof pkiAlertServiceFactory>; export type TPkiAlertServiceFactory = ReturnType<typeof pkiAlertServiceFactory>;
@@ -19,8 +23,42 @@ export type TPkiAlertServiceFactory = ReturnType<typeof pkiAlertServiceFactory>;
export const pkiAlertServiceFactory = ({ export const pkiAlertServiceFactory = ({
pkiAlertDAL, pkiAlertDAL,
pkiCollectionDAL, pkiCollectionDAL,
permissionService permissionService,
smtpService
}: TPkiAlertServiceFactoryDep) => { }: 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 ({ const createPkiAlert = async ({
projectId, projectId,
name, name,
@@ -108,7 +146,7 @@ export const pkiAlertServiceFactory = ({
name, name,
alertBeforeDays, alertBeforeDays,
...(pkiCollectionId && { pkiCollectionId }), ...(pkiCollectionId && { pkiCollectionId }),
...(emails && { recipientEmails: emails.join(",") }) // TODO: standardize recipient emails ...(emails && { recipientEmails: emails.join(",") })
}); });
return alert; return alert;
@@ -132,6 +170,7 @@ export const pkiAlertServiceFactory = ({
}; };
return { return {
sendPkiItemExpiryNotices,
createPkiAlert, createPkiAlert,
getPkiAlertById, getPkiAlertById,
updatePkiAlert, updatePkiAlert,

View File

@@ -1,13 +1,72 @@
import { TDbClient } from "@app/db"; 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 { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify } from "@app/lib/knex";
import { PkiItemType } from "./pki-collection-types";
export type TPkiCollectionItemDALFactory = ReturnType<typeof pkiCollectionItemDALFactory>; export type TPkiCollectionItemDALFactory = ReturnType<typeof pkiCollectionItemDALFactory>;
export const pkiCollectionItemDALFactory = (db: TDbClient) => { export const pkiCollectionItemDALFactory = (db: TDbClient) => {
const pkiCollectionItemOrm = ormify(db, TableName.PkiCollectionItem); 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) => { const countItemsInPkiCollection = async (collectionId: string) => {
try { try {
interface CountResult { interface CountResult {
@@ -28,6 +87,7 @@ export const pkiCollectionItemDALFactory = (db: TDbClient) => {
return { return {
...pkiCollectionItemOrm, ...pkiCollectionItemOrm,
findPkiCollectionItems,
countItemsInPkiCollection countItemsInPkiCollection
}; };
}; };

View File

@@ -144,6 +144,7 @@ export const pkiCollectionServiceFactory = ({
const getPkiCollectionItems = async ({ const getPkiCollectionItems = async ({
collectionId, collectionId,
type,
offset = 0, offset = 0,
limit = 25, limit = 25,
actorId, actorId,
@@ -164,16 +165,23 @@ export const pkiCollectionServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
const pkiCollectionItems = await pkiCollectionItemDAL.find( const pkiCollectionItems = await pkiCollectionItemDAL.findPkiCollectionItems({
{ pkiCollectionId: collectionId }, collectionId,
{ offset, limit, sort: [["createdAt", "desc"]] } type,
); offset,
limit
});
const count = await pkiCollectionItemDAL.countItemsInPkiCollection(collectionId); const count = await pkiCollectionItemDAL.countItemsInPkiCollection(collectionId);
return { return {
pkiCollection, pkiCollection,
pkiCollectionItems: pkiCollectionItems.map(transformPkiCollectionItem), pkiCollectionItems: pkiCollectionItems.map((p) => ({
...transformPkiCollectionItem(p),
notBefore: p.notBefore,
notAfter: p.notAfter,
friendlyName: p.friendlyName
})),
totalCount: count totalCount: count
}; };
}; };

View File

@@ -22,8 +22,14 @@ export enum PkiItemType {
CA = "ca" CA = "ca"
} }
export const pkiItemTypeToNameMap: { [K in PkiItemType]: string } = {
[PkiItemType.CA]: "CA",
[PkiItemType.CERTIFICATE]: "Certificate"
};
export type TGetPkiCollectionItems = { export type TGetPkiCollectionItems = {
collectionId: string; collectionId: string;
type?: PkiItemType;
offset: number; offset: number;
limit: number; limit: number;
} & Omit<TProjectPermission, "projectId">; } & 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 { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; 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 { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TSecretVersionDALFactory } from "../secret/secret-version-dal"; import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
@@ -18,6 +19,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">; snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">; secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">;
queueService: TQueueServiceFactory; queueService: TQueueServiceFactory;
pkiAlertService: Pick<TPkiAlertServiceFactory, "sendPkiItemExpiryNotices">;
}; };
export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyResourceCleanUpQueueServiceFactory>; export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyResourceCleanUpQueueServiceFactory>;
@@ -25,6 +27,7 @@ export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyRe
export const dailyResourceCleanUpQueueServiceFactory = ({ export const dailyResourceCleanUpQueueServiceFactory = ({
auditLogDAL, auditLogDAL,
queueService, queueService,
pkiAlertService,
snapshotDAL, snapshotDAL,
secretVersionDAL, secretVersionDAL,
secretFolderVersionDAL, secretFolderVersionDAL,
@@ -41,6 +44,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
await secretVersionDAL.pruneExcessVersions(); await secretVersionDAL.pruneExcessVersions();
await secretVersionV2DAL.pruneExcessVersions(); await secretVersionV2DAL.pruneExcessVersions();
await secretFolderVersionDAL.pruneExcessVersions(); await secretFolderVersionDAL.pruneExcessVersions();
await pkiAlertService.sendPkiItemExpiryNotices();
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`); logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
}); });

View File

@@ -31,7 +31,8 @@ export enum SmtpTemplates {
ResetPassword = "passwordReset.handlebars", ResetPassword = "passwordReset.handlebars",
SecretLeakIncident = "secretLeakIncident.handlebars", SecretLeakIncident = "secretLeakIncident.handlebars",
WorkspaceInvite = "workspaceInvitation.handlebars", WorkspaceInvite = "workspaceInvitation.handlebars",
ScimUserProvisioned = "scimUserProvisioned.handlebars" ScimUserProvisioned = "scimUserProvisioned.handlebars",
PkiExpirationAlert = "pkiExpirationAlert.handlebars"
} }
export enum SmtpHost { 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": [ "pages": [
"documentation/platform/pki/overview", "documentation/platform/pki/overview",
"documentation/platform/pki/private-ca", "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/delete",
"api-reference/endpoints/certificates/cert-body" "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 { apiRequest } from "@app/config/request";
import { PkiItemType } from "./constants";
import { TPkiCollection, TPkiCollectionItem } from "./types"; import { TPkiCollection, TPkiCollectionItem } from "./types";
export const pkiCollectionKeys = { export const pkiCollectionKeys = {
@@ -10,16 +11,18 @@ export const pkiCollectionKeys = {
[{ collectionId }, "pki-collection-items"] as const, [{ collectionId }, "pki-collection-items"] as const,
specificPkiCollectionItems: ({ specificPkiCollectionItems: ({
collectionId, collectionId,
type,
offset, offset,
limit limit
}: { }: {
collectionId: string; collectionId: string;
type?: PkiItemType;
offset: number; offset: number;
limit: number; limit: number;
}) => }) =>
[ [
...pkiCollectionKeys.getPkiCollectionItems(collectionId), ...pkiCollectionKeys.getPkiCollectionItems(collectionId),
{ offset, limit }, { offset, limit, type },
"pki-collection-items-2" "pki-collection-items-2"
] as const ] as const
}; };
@@ -39,10 +42,12 @@ export const useGetPkiCollectionById = (collectionId: string) => {
export const useListPkiCollectionItems = ({ export const useListPkiCollectionItems = ({
collectionId, collectionId,
type,
offset, offset,
limit limit
}: { }: {
collectionId: string; collectionId: string;
type?: PkiItemType;
offset: number; offset: number;
limit: number; limit: number;
}) => { }) => {
@@ -50,18 +55,24 @@ export const useListPkiCollectionItems = ({
queryKey: pkiCollectionKeys.specificPkiCollectionItems({ queryKey: pkiCollectionKeys.specificPkiCollectionItems({
collectionId, collectionId,
offset, offset,
limit limit,
type
}), }),
queryFn: async () => { queryFn: async () => {
const params = new URLSearchParams({ const params = new URLSearchParams({
offset: String(offset), offset: String(offset),
limit: String(limit) limit: String(limit),
...(type ? { type } : {})
}); });
const { const {
data: { collectionItems, totalCount } data: { collectionItems, totalCount }
} = await apiRequest.get<{ } = await apiRequest.get<{
collectionItems: TPkiCollectionItem[]; collectionItems: (TPkiCollectionItem & {
notBefore: string;
notAfter: string;
friendlyName: string;
})[];
totalCount: number; totalCount: number;
}>(`/api/v1/pki/collections/${collectionId}/items`, { }>(`/api/v1/pki/collections/${collectionId}/items`, {
params params

View File

@@ -11,8 +11,7 @@ import {
Modal, Modal,
ModalContent, ModalContent,
Select, Select,
SelectItem, SelectItem
TextArea
} from "@app/components/v2"; } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { import {
@@ -193,7 +192,7 @@ export const PkiAlertModal = ({ popUp, handlePopUpToggle }: Props) => {
defaultValue="" defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl <FormControl
label="PKI Collection" label="Certificate Collection"
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
isRequired isRequired
@@ -262,16 +261,12 @@ export const PkiAlertModal = ({ popUp, handlePopUpToggle }: Props) => {
name="emails" name="emails"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Recipient Email(s)" label="Emails to Alert"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
isRequired isRequired
> >
<TextArea <Input {...field} placeholder="johndoe@gmail.com, janedoe@gmail.com, ..." />
{...field}
placeholder="aturing@gmail.com, alovelace@gmail.com, ..."
reSize="none"
/>
</FormControl> </FormControl>
)} )}
/> />

View File

@@ -17,7 +17,8 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc"; 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 { usePopUp } from "@app/hooks/usePopUp";
import { PkiCollectionModal } from "../CertificatesPage/components/PkiAlertsTab/components/PkiCollectionModal"; import { PkiCollectionModal } from "../CertificatesPage/components/PkiAlertsTab/components/PkiCollectionModal";
@@ -137,7 +138,15 @@ export const PkiCollectionPage = withProjectPermission(
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
/> />
</div> </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>
</div> </div>
)} )}

View File

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

View File

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

View File

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