diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 336776dbf..d79faa647 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1088,6 +1088,7 @@ export const CERTIFICATE_AUTHORITIES = { }, ISSUE_CERT: { caId: "The ID of the CA to issue the certificate from", + pkiCollectionId: "The ID of the PKI collection to add the certificate to", friendlyName: "A friendly name for the certificate", commonName: "The common name (CN) for the certificate", altNames: @@ -1103,6 +1104,7 @@ export const CERTIFICATE_AUTHORITIES = { }, SIGN_CERT: { caId: "The ID of the CA to issue the certificate from", + pkiCollectionId: "The ID of the PKI collection to add the certificate to", csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance", friendlyName: "A friendly name for the certificate", commonName: "The common name (CN) for the certificate", diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index dbd223d7b..41d5f3249 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -622,6 +622,8 @@ export const registerRoutes = async ( certificateAuthorityQueue, certificateDAL, certificateBodyDAL, + pkiCollectionDAL, + pkiCollectionItemDAL, projectDAL, kmsService, permissionService diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index 103d430c0..35de2b953 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -556,6 +556,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }), body: z .object({ + pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.pkiCollectionId), friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName), commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName), altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames), @@ -635,6 +636,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { body: z .object({ csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.csr), + pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.pkiCollectionId), friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.friendlyName), commonName: z.string().trim().min(1).optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.commonName), altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.altNames), diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index b63ec9ba9..88f686089 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -7,10 +7,12 @@ import { z } from "zod"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { BadRequestError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal"; +import { TPkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; @@ -60,6 +62,8 @@ type TCertificateAuthorityServiceFactoryDep = { certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick certificateDAL: Pick; certificateBodyDAL: Pick; + pkiCollectionDAL: Pick; + pkiCollectionItemDAL: Pick; projectDAL: Pick; kmsService: Pick; permissionService: Pick; @@ -74,6 +78,8 @@ export const certificateAuthorityServiceFactory = ({ certificateAuthorityCrlDAL, certificateDAL, certificateBodyDAL, + pkiCollectionDAL, + pkiCollectionItemDAL, projectDAL, kmsService, permissionService @@ -1007,6 +1013,7 @@ export const certificateAuthorityServiceFactory = ({ */ const issueCertFromCa = async ({ caId, + pkiCollectionId, friendlyName, commonName, altNames, @@ -1039,6 +1046,13 @@ export const certificateAuthorityServiceFactory = ({ throw new BadRequestError({ message: "CA is expired" }); } + // check PKI collection + if (pkiCollectionId) { + const pkiCollection = await pkiCollectionDAL.findById(pkiCollectionId); + if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" }); + if (pkiCollection.projectId !== ca.projectId) throw new BadRequestError({ message: "Invalid PKI collection" }); + } + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, projectDAL, @@ -1186,6 +1200,16 @@ export const certificateAuthorityServiceFactory = ({ tx ); + if (pkiCollectionId) { + await pkiCollectionItemDAL.create( + { + pkiCollectionId, + certId: cert.id + }, + tx + ); + } + return cert; }); @@ -1214,6 +1238,7 @@ export const certificateAuthorityServiceFactory = ({ const signCertFromCa = async ({ caId, csr, + pkiCollectionId, friendlyName, commonName, altNames, @@ -1247,6 +1272,13 @@ export const certificateAuthorityServiceFactory = ({ throw new BadRequestError({ message: "CA is expired" }); } + // check PKI collection + if (pkiCollectionId) { + const pkiCollection = await pkiCollectionDAL.findById(pkiCollectionId); + if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" }); + if (pkiCollection.projectId !== ca.projectId) throw new BadRequestError({ message: "Invalid PKI collection" }); + } + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, projectDAL, @@ -1390,6 +1422,16 @@ export const certificateAuthorityServiceFactory = ({ tx ); + if (pkiCollectionId) { + await pkiCollectionItemDAL.create( + { + pkiCollectionId, + certId: cert.id + }, + tx + ); + } + return cert; }); diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index 31a6e1629..3fef204bf 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -87,6 +87,7 @@ export type TImportCertToCaDTO = { export type TIssueCertFromCaDTO = { caId: string; + pkiCollectionId?: string; friendlyName?: string; commonName: string; altNames: string; @@ -98,6 +99,7 @@ export type TIssueCertFromCaDTO = { export type TSignCertFromCaDTO = { caId: string; csr: string; + pkiCollectionId?: string; friendlyName?: string; commonName?: string; altNames: string; diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts index 7513070c8..692714871 100644 --- a/frontend/src/hooks/api/ca/types.ts +++ b/frontend/src/hooks/api/ca/types.ts @@ -80,6 +80,7 @@ export type TImportCaCertificateResponse = { export type TCreateCertificateDTO = { projectSlug: string; caId: string; + pkiCollectionId?: string; friendlyName?: string; commonName: string; altNames: string; // sans diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx index 12bd8c47c..227abe02a 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx @@ -14,7 +14,13 @@ import { SelectItem } from "@app/components/v2"; import { useWorkspace } from "@app/context"; -import { CaStatus, useCreateCertificate, useGetCert, useListWorkspaceCas } from "@app/hooks/api"; +import { + CaStatus, + useCreateCertificate, + useGetCert, + useListWorkspaceCas, + useListWorkspacePkiCollections +} from "@app/hooks/api"; import { caTypeToNameMap } from "@app/hooks/api/ca/constants"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -22,6 +28,7 @@ import { CertificateContent } from "./CertificateContent"; const schema = z.object({ caId: z.string(), + collectionId: z.string().optional(), friendlyName: z.string(), commonName: z.string().trim().min(1), altNames: z.string(), @@ -54,6 +61,10 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { status: CaStatus.ACTIVE }); + const { data } = useListWorkspacePkiCollections({ + workspaceId: currentWorkspace?.id || "" + }); + const { mutateAsync: createCertificate } = useCreateCertificate(); const { @@ -86,13 +97,21 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { } }, [cert]); - const onFormSubmit = async ({ caId, friendlyName, commonName, altNames, ttl }: FormData) => { + const onFormSubmit = async ({ + caId, + collectionId, + friendlyName, + commonName, + altNames, + ttl + }: FormData) => { try { if (!currentWorkspace?.slug) return; const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({ projectSlug: currentWorkspace.slug, caId, + pkiCollectionId: collectionId, friendlyName, commonName, altNames, @@ -167,6 +186,32 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { )} /> + ( + + + + )} + />