From a2a786f392203d6d42315a71bf603fee0abe8d4f Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 29 May 2024 16:12:46 -0700 Subject: [PATCH] Transfer cert endpoints to work with serial numbers --- .../20240522010055_certificate-authority.ts | 5 +- .../src/db/schemas/certificate-authorities.ts | 1 + backend/src/db/schemas/certificates.ts | 2 + .../server/routes/v1/certificate-router.ts | 58 +++++++++++--- backend/src/server/routes/v1/index.ts | 9 ++- .../certificate-authority-service.ts | 34 ++++++-- .../certificate-authority-types.ts | 2 - .../certificate/certificate-service.ts | 46 ++++++++--- .../services/certificate/certificate-types.ts | 15 +++- .../src/services/project/project-service.ts | 2 +- frontend/src/hooks/api/ca/mutations.tsx | 15 ++-- frontend/src/hooks/api/ca/queries.tsx | 6 +- .../src/hooks/api/certificates/constants.tsx | 6 ++ frontend/src/hooks/api/certificates/enums.tsx | 4 + frontend/src/hooks/api/certificates/index.tsx | 4 +- .../src/hooks/api/certificates/mutations.tsx | 32 +++++--- .../src/hooks/api/certificates/queries.tsx | 22 +++--- frontend/src/hooks/api/certificates/types.ts | 14 ++++ .../components/CertificateCertModal.tsx | 2 +- .../components/CertificateContent.tsx | 74 +++++++++-------- .../components/CertificateModal.tsx | 11 +-- .../components/CertificatesSection.tsx | 79 ++++++++++++++----- .../components/CertificatesTable.tsx | 42 ++++++++-- 23 files changed, 346 insertions(+), 139 deletions(-) create mode 100644 frontend/src/hooks/api/certificates/constants.tsx create mode 100644 frontend/src/hooks/api/certificates/enums.tsx diff --git a/backend/src/db/migrations/20240522010055_certificate-authority.ts b/backend/src/db/migrations/20240522010055_certificate-authority.ts index bf887beb9..b8e3eb171 100644 --- a/backend/src/db/migrations/20240522010055_certificate-authority.ts +++ b/backend/src/db/migrations/20240522010055_certificate-authority.ts @@ -23,6 +23,7 @@ export async function up(knex: Knex): Promise { t.string("commonName").notNullable(); t.string("dn").notNullable(); t.unique(["dn", "projectId"]); + t.string("serialNumber").nullable().unique(); t.integer("maxPathLength").nullable(); t.datetime("notBefore").nullable(); t.datetime("notAfter").nullable(); @@ -54,12 +55,14 @@ export async function up(knex: Knex): Promise { } if (!(await knex.schema.hasTable(TableName.Certificate))) { - // TODO: consider adding name + // TODO: consider adding serialNumber await knex.schema.createTable(TableName.Certificate, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.timestamps(true, true, true); t.uuid("caId").notNullable(); t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE"); + t.string("status").notNullable(); // active / pending-certificate + t.string("serialNumber").notNullable().unique(); t.string("commonName").notNullable(); t.datetime("notBefore").notNullable(); t.datetime("notAfter").notNullable(); diff --git a/backend/src/db/schemas/certificate-authorities.ts b/backend/src/db/schemas/certificate-authorities.ts index da69dd5a5..7e2d147f4 100644 --- a/backend/src/db/schemas/certificate-authorities.ts +++ b/backend/src/db/schemas/certificate-authorities.ts @@ -22,6 +22,7 @@ export const CertificateAuthoritiesSchema = z.object({ locality: z.string(), commonName: z.string(), dn: z.string(), + serialNumber: z.string().nullable().optional(), maxPathLength: z.number().nullable().optional(), notBefore: z.date().nullable().optional(), notAfter: z.date().nullable().optional() diff --git a/backend/src/db/schemas/certificates.ts b/backend/src/db/schemas/certificates.ts index 74486258d..d64b17d8e 100644 --- a/backend/src/db/schemas/certificates.ts +++ b/backend/src/db/schemas/certificates.ts @@ -12,6 +12,8 @@ export const CertificatesSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), caId: z.string().uuid(), + status: z.string(), + serialNumber: z.string(), commonName: z.string(), notBefore: z.date(), notAfter: z.date() diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index 305afebf9..8963c3940 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -8,7 +8,7 @@ import { AuthMode } from "@app/services/auth/auth-type"; export const registerCertRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:certId", + url: "/:serialNumber", config: { rateLimit: readLimit }, @@ -16,7 +16,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { schema: { description: "Get certificate", params: z.object({ - certId: z.string().trim() + serialNumber: z.string().trim() }), response: { 200: z.object({ @@ -25,8 +25,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const certificate = await server.services.certificate.getCertById({ - certId: req.params.certId, + const certificate = await server.services.certificate.getCert({ + serialNumber: req.params.serialNumber, actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -38,9 +38,45 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "POST", + url: "/:serialNumber/revoke", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Revoke", + params: z.object({ + serialNumber: z.string().trim() + }), + response: { + 200: z.object({ + message: z.string().trim(), + serialNumber: z.string().trim(), + revokedAt: z.date() + }) + } + }, + handler: async (req) => { + await server.services.certificate.revokeCert({ + serialNumber: req.params.serialNumber, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + return { + message: "Successfully revoked certificate", + serialNumber: req.params.serialNumber, + revokedAt: new Date() + }; + } + }); + server.route({ method: "DELETE", - url: "/:certId", + url: "/:serialNumber", config: { rateLimit: writeLimit }, @@ -48,7 +84,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { schema: { description: "Delete certificate", params: z.object({ - certId: z.string().trim() + serialNumber: z.string().trim() }), response: { 200: z.object({ @@ -57,8 +93,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const certificate = await server.services.certificate.deleteCertById({ - certId: req.params.certId, + const certificate = await server.services.certificate.deleteCert({ + serialNumber: req.params.serialNumber, actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -72,7 +108,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:certId/certificate", + url: "/:serialNumber/certificate", config: { rateLimit: readLimit }, @@ -80,7 +116,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { schema: { description: "Get certificate of certificate", params: z.object({ - certId: z.string().trim() + serialNumber: z.string().trim() }), response: { 200: z.object({ @@ -92,7 +128,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, handler: async (req) => { const { certificate, certificateChain, serialNumber } = await server.services.certificate.getCertCert({ - certId: req.params.certId, + serialNumber: req.params.serialNumber, actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 61ef947ac..52248ac63 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -60,8 +60,13 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { { prefix: "/workspace" } ); - await server.register(registerCaRouter, { prefix: "/ca" }); - await server.register(registerCertRouter, { prefix: "/certificates" }); + await server.register( + async (pkiRouter) => { + await pkiRouter.register(registerCaRouter, { prefix: "/ca" }); + await pkiRouter.register(registerCertRouter, { prefix: "/certificates" }); + }, + { prefix: "/pki" } + ); await server.register(registerProjectBotRouter, { prefix: "/bot" }); await server.register(registerIntegrationRouter, { prefix: "/integration" }); diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 7d9ea02f8..1fdfbfdb5 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-bitwise */ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import crypto, { KeyObject } from "crypto"; @@ -9,6 +10,7 @@ import { TCertificateCertDALFactory } from "@app/services/certificate/certificat import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { TCertStatus } from "../certificate/certificate-types"; import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal"; import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; import { createDistinguishedName } from "./certificate-authority-fns"; @@ -118,6 +120,7 @@ export const certificateAuthorityServiceFactory = ({ ? new Date(notAfter) : new Date(new Date().setFullYear(new Date().getFullYear() + 10)); + const serialNumber = crypto.randomBytes(32).toString("hex"); const ca = await certificateAuthorityDAL.create( { projectId: project.id, @@ -130,7 +133,7 @@ export const certificateAuthorityServiceFactory = ({ commonName, status: type === CaType.ROOT ? CaStatus.ACTIVE : CaStatus.PENDING_CERTIFICATE, dn, - ...(type === CaType.ROOT && { maxPathLength, notBefore: notBeforeDate, notAfter: notAfterDate }) + ...(type === CaType.ROOT && { maxPathLength, notBefore: notBeforeDate, notAfter: notAfterDate, serialNumber }) }, tx ); @@ -140,6 +143,7 @@ export const certificateAuthorityServiceFactory = ({ const cert = await x509.X509CertificateGenerator.createSelfSigned({ name: dn, + serialNumber, notBefore: notBeforeDate, notAfter: notAfterDate, signingAlgorithm: alg, @@ -291,7 +295,12 @@ export const certificateAuthorityServiceFactory = ({ signingAlgorithm: alg, extensions: [ // eslint-disable-next-line no-bitwise - new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment) + new x509.KeyUsagesExtension( + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment + ) ], attributes: [new x509.ChallengePasswordAttribute("password")] }); @@ -411,8 +420,9 @@ export const certificateAuthorityServiceFactory = ({ throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); } + const serialNumber = crypto.randomBytes(32).toString("hex"); const intermediateCert = await x509.X509CertificateGenerator.create({ - // serialNumber: "03", + serialNumber, subject: csrObj.subject, issuer: certObj.subject, notBefore: notBeforeDate, @@ -421,7 +431,13 @@ export const certificateAuthorityServiceFactory = ({ publicKey: csrObj.publicKey, signingAlgorithm: alg, extensions: [ - new x509.KeyUsagesExtension(x509.KeyUsageFlags.dataEncipherment, true), + new x509.KeyUsagesExtension( + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true), await x509.AuthorityKeyIdentifierExtension.create(certObj, false), await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) @@ -511,6 +527,7 @@ export const certificateAuthorityServiceFactory = ({ maxPathLength: maxPathLength === undefined ? -1 : maxPathLength, notBefore: new Date(certObj.notBefore), notAfter: new Date(certObj.notAfter), + serialNumber: certObj.serialNumber, parentCaId: parentCa?.id }, tx @@ -601,8 +618,9 @@ export const certificateAuthorityServiceFactory = ({ throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); } + const serialNumber = crypto.randomBytes(32).toString("hex"); const leafCert = await x509.X509CertificateGenerator.create({ - // serialNumber: "03", + serialNumber, subject: csrObj.subject, issuer: caCertObj.subject, notBefore: notBeforeDate, @@ -611,7 +629,7 @@ export const certificateAuthorityServiceFactory = ({ publicKey: csrObj.publicKey, signingAlgorithm: alg, extensions: [ - new x509.KeyUsagesExtension(x509.KeyUsageFlags.dataEncipherment, true), + new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true), new x509.BasicConstraintsExtension(false), await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) @@ -627,7 +645,9 @@ export const certificateAuthorityServiceFactory = ({ const cert = await certificateDAL.create( { caId: ca.id, + status: TCertStatus.ACTIVE, commonName, + serialNumber, notBefore: notBeforeDate, notAfter: notAfterDate }, @@ -651,7 +671,7 @@ export const certificateAuthorityServiceFactory = ({ certificateChain: chain.join("\n"), issuingCaCertificate: caCert.certificate, privateKey: skLeaf, - serialNumber: leafCert.serialNumber + serialNumber }; }; diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index 5c3a22e35..dbb0c52c9 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -11,8 +11,6 @@ export enum CaStatus { PENDING_CERTIFICATE = "pending-certificate" } -// TODO: attach permissions after draft impl - export type TCreateCaDTO = { projectSlug: string; type: CaType; diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index 09e2cc0fe..fee486144 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -7,10 +7,10 @@ import { TCertificateCertDALFactory } from "@app/services/certificate/certificat import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; -import { TDeleteCertDTO, TGetCertCertDTO, TGetCertDTO } from "./certificate-types"; +import { TDeleteCertDTO, TGetCertCertDTO, TGetCertDTO, TRevokeCertDTO } from "./certificate-types"; type TCertificateServiceFactoryDep = { - certificateDAL: Pick; + certificateDAL: Pick; certificateCertDAL: Pick; certificateAuthorityDAL: Pick; permissionService: Pick; @@ -24,8 +24,8 @@ export const certificateServiceFactory = ({ certificateAuthorityDAL, permissionService }: TCertificateServiceFactoryDep) => { - const getCertById = async ({ certId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => { - const cert = await certificateDAL.findById(certId); + const getCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => { + const cert = await certificateDAL.findOne({ serialNumber }); const ca = await certificateAuthorityDAL.findById(cert.caId); const { permission } = await permissionService.getProjectPermission( @@ -41,8 +41,8 @@ export const certificateServiceFactory = ({ return cert; }; - const deleteCertById = async ({ certId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertDTO) => { - const cert = await certificateDAL.findById(certId); + const deleteCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertDTO) => { + const cert = await certificateDAL.findOne({ serialNumber }); const ca = await certificateAuthorityDAL.findById(cert.caId); const { permission } = await permissionService.getProjectPermission( @@ -59,8 +59,31 @@ export const certificateServiceFactory = ({ return deletedCert; }; - const getCertCert = async ({ certId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertCertDTO) => { - const cert = await certificateDAL.findById(certId); + const revokeCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TRevokeCertDTO) => { + const cert = await certificateDAL.findOne({ serialNumber }); + const ca = await certificateAuthorityDAL.findById(cert.caId); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + ca.projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates); + // WIP + + // const revocationDate = new Date(); + + // const serialNumber2 = crypto.randomBytes(16).toString("hex"); + // const crlEntry = new x509.X509CrlEntry(serialNumber2, new Date(), []); + + return {}; + }; + + const getCertCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertCertDTO) => { + const cert = await certificateDAL.findOne({ serialNumber }); const ca = await certificateAuthorityDAL.findById(cert.caId); const { permission } = await permissionService.getProjectPermission( @@ -73,7 +96,7 @@ export const certificateServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); - const certCert = await certificateCertDAL.findOne({ certId }); + const certCert = await certificateCertDAL.findOne({ certId: cert.id }); const certObj = new x509.X509Certificate(certCert.certificate); return { @@ -84,8 +107,9 @@ export const certificateServiceFactory = ({ }; return { - getCertById, - deleteCertById, + getCert, + deleteCert, + revokeCert, getCertCert }; }; diff --git a/backend/src/services/certificate/certificate-types.ts b/backend/src/services/certificate/certificate-types.ts index 1389896c6..6ba8b796e 100644 --- a/backend/src/services/certificate/certificate-types.ts +++ b/backend/src/services/certificate/certificate-types.ts @@ -1,13 +1,22 @@ import { TProjectPermission } from "@app/lib/types"; +export enum TCertStatus { + ACTIVE = "active", + REVOKED = "revoked" +} + export type TGetCertDTO = { - certId: string; + serialNumber: string; } & Omit; export type TDeleteCertDTO = { - certId: string; + serialNumber: string; +} & Omit; + +export type TRevokeCertDTO = { + serialNumber: string; } & Omit; export type TGetCertCertDTO = { - certId: string; + serialNumber: string; } & Omit; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 961a57985..753b1efba 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -67,7 +67,7 @@ type TProjectServiceFactoryDep = { projectUserMembershipRoleDAL: Pick; secretBlindIndexDAL: Pick; certificateAuthorityDAL: Pick; - certificateDAL: TCertificateDALFactory; + certificateDAL: Pick; permissionService: TPermissionServiceFactory; orgService: Pick; licenseService: Pick; diff --git a/frontend/src/hooks/api/ca/mutations.tsx b/frontend/src/hooks/api/ca/mutations.tsx index 49de70d4f..3a8f18e17 100644 --- a/frontend/src/hooks/api/ca/mutations.tsx +++ b/frontend/src/hooks/api/ca/mutations.tsx @@ -13,7 +13,8 @@ import { TImportCaCertificateResponse, TSignIntermediateDTO, TSignIntermediateResponse, - TUpdateCaDTO} from "./types"; + TUpdateCaDTO +} from "./types"; export const useCreateCa = () => { const queryClient = useQueryClient(); @@ -21,7 +22,7 @@ export const useCreateCa = () => { mutationFn: async (body) => { const { data: { ca } - } = await apiRequest.post<{ ca: TCertificateAuthority }>("/api/v1/ca/", body); + } = await apiRequest.post<{ ca: TCertificateAuthority }>("/api/v1/pki/ca/", body); return ca; }, onSuccess: (_, { projectSlug }) => { @@ -36,7 +37,7 @@ export const useUpdateCa = () => { mutationFn: async ({ caId, projectSlug, ...body }) => { const { data: { ca } - } = await apiRequest.patch<{ ca: TCertificateAuthority }>(`/api/v1/ca/${caId}`, body); + } = await apiRequest.patch<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`, body); return ca; }, onSuccess: (_, { projectSlug }) => { @@ -51,7 +52,7 @@ export const useDeleteCa = () => { mutationFn: async ({ caId }) => { const { data: { ca } - } = await apiRequest.delete<{ ca: TCertificateAuthority }>(`/api/v1/ca/${caId}`); + } = await apiRequest.delete<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`); return ca; }, onSuccess: (_, { projectSlug }) => { @@ -65,7 +66,7 @@ export const useSignIntermediate = () => { return useMutation({ mutationFn: async (body) => { const { data } = await apiRequest.post( - `/api/v1/ca/${body.caId}/sign-intermediate`, + `/api/v1/pki/ca/${body.caId}/sign-intermediate`, body ); return data; @@ -78,7 +79,7 @@ export const useImportCaCertificate = () => { return useMutation({ mutationFn: async ({ caId, ...body }) => { const { data } = await apiRequest.post( - `/api/v1/ca/${caId}/import-certificate`, + `/api/v1/pki/ca/${caId}/import-certificate`, body ); return data; @@ -95,7 +96,7 @@ export const useCreateCertificate = () => { return useMutation({ mutationFn: async ({ caId, ...body }) => { const { data } = await apiRequest.post( - `/api/v1/ca/${caId}/issue-certificate`, + `/api/v1/pki/ca/${caId}/issue-certificate`, body ); return data; diff --git a/frontend/src/hooks/api/ca/queries.tsx b/frontend/src/hooks/api/ca/queries.tsx index 838f50726..a0e64fd4e 100644 --- a/frontend/src/hooks/api/ca/queries.tsx +++ b/frontend/src/hooks/api/ca/queries.tsx @@ -16,7 +16,7 @@ export const useGetCaById = (caId: string) => { queryFn: async () => { const { data: { ca } - } = await apiRequest.get<{ ca: TCertificateAuthority }>(`/api/v1/ca/${caId}`); + } = await apiRequest.get<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`); return ca; }, enabled: Boolean(caId) @@ -31,7 +31,7 @@ export const useGetCaCert = (caId: string) => { certificate: string; certificateChain: string; serialNumber: string; - }>(`/api/v1/ca/${caId}/certificate`); + }>(`/api/v1/pki/ca/${caId}/certificate`); return data; }, enabled: Boolean(caId) @@ -46,7 +46,7 @@ export const useGetCaCsr = (caId: string) => { data: { csr } } = await apiRequest.get<{ csr: string; - }>(`/api/v1/ca/${caId}/csr`); + }>(`/api/v1/pki/ca/${caId}/csr`); return csr; }, enabled: Boolean(caId) diff --git a/frontend/src/hooks/api/certificates/constants.tsx b/frontend/src/hooks/api/certificates/constants.tsx new file mode 100644 index 000000000..6188006e6 --- /dev/null +++ b/frontend/src/hooks/api/certificates/constants.tsx @@ -0,0 +1,6 @@ +import { CertStatus } from "./enums"; + +export const certStatusToNameMap: { [K in CertStatus]: string } = { + [CertStatus.ACTIVE]: "Active", + [CertStatus.REVOKED]: "Revoked" +}; diff --git a/frontend/src/hooks/api/certificates/enums.tsx b/frontend/src/hooks/api/certificates/enums.tsx new file mode 100644 index 000000000..5bab7ce1b --- /dev/null +++ b/frontend/src/hooks/api/certificates/enums.tsx @@ -0,0 +1,4 @@ +export enum CertStatus { + ACTIVE = "active", + REVOKED = "revoked" +} diff --git a/frontend/src/hooks/api/certificates/index.tsx b/frontend/src/hooks/api/certificates/index.tsx index 095d7d10a..df3f39fc7 100644 --- a/frontend/src/hooks/api/certificates/index.tsx +++ b/frontend/src/hooks/api/certificates/index.tsx @@ -1,2 +1,2 @@ -export { useDeleteCert } from "./mutations"; -export { useGetCertById, useGetCertCert } from "./queries"; +export { useDeleteCert, useRevokeCert } from "./mutations"; +export { useGetCert, useGetCertCert } from "./queries"; diff --git a/frontend/src/hooks/api/certificates/mutations.tsx b/frontend/src/hooks/api/certificates/mutations.tsx index 0e128c37d..ae2a6a745 100644 --- a/frontend/src/hooks/api/certificates/mutations.tsx +++ b/frontend/src/hooks/api/certificates/mutations.tsx @@ -3,20 +3,34 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { workspaceKeys } from "../workspace/queries"; -import { TCertificate } from "./types"; - -export type TDeleteCaDTO = { - projectSlug: string; - certId: string; -}; +import { TCertificate, TDeleteCertDTO, TRevokeCertDTO } from "./types"; export const useDeleteCert = () => { const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async ({ certId }) => { + return useMutation({ + mutationFn: async ({ serialNumber }) => { const { data: { certificate } - } = await apiRequest.delete<{ certificate: TCertificate }>(`/api/v1/certificates/${certId}`); + } = await apiRequest.delete<{ certificate: TCertificate }>( + `/api/v1/pki/certificates/${serialNumber}` + ); + return certificate; + }, + onSuccess: (_, { projectSlug }) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificates(projectSlug)); + } + }); +}; + +export const useRevokeCert = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ serialNumber }) => { + const { + data: { certificate } + } = await apiRequest.post<{ certificate: TCertificate }>( + `/api/v1/pki/certificates/${serialNumber}/revoke` + ); return certificate; }, onSuccess: (_, { projectSlug }) => { diff --git a/frontend/src/hooks/api/certificates/queries.tsx b/frontend/src/hooks/api/certificates/queries.tsx index e07ff27e7..86ae3d4be 100644 --- a/frontend/src/hooks/api/certificates/queries.tsx +++ b/frontend/src/hooks/api/certificates/queries.tsx @@ -5,34 +5,36 @@ import { apiRequest } from "@app/config/request"; import { TCertificate } from "./types"; export const certKeys = { - getCertById: (certId: string) => [{ certId }, "cert"], - getCertCert: (certId: string) => [{ certId }, "certCert"] + getCertById: (serialNumber: string) => [{ serialNumber }, "cert"], + getCertCert: (serialNumber: string) => [{ serialNumber }, "certCert"] }; -export const useGetCertById = (certId: string) => { +export const useGetCert = (serialNumber: string) => { return useQuery({ - queryKey: certKeys.getCertById(certId), + queryKey: certKeys.getCertById(serialNumber), queryFn: async () => { const { data: { certificate } - } = await apiRequest.get<{ certificate: TCertificate }>(`/api/v1/certificates/${certId}`); + } = await apiRequest.get<{ certificate: TCertificate }>( + `/api/v1/pki/certificates/${serialNumber}` + ); return certificate; }, - enabled: Boolean(certId) + enabled: Boolean(serialNumber) }); }; -export const useGetCertCert = (certId: string) => { +export const useGetCertCert = (serialNumber: string) => { return useQuery({ - queryKey: certKeys.getCertCert(certId), + queryKey: certKeys.getCertCert(serialNumber), queryFn: async () => { const { data } = await apiRequest.get<{ certificate: string; certificateChain: string; serialNumber: string; - }>(`/api/v1/certificates/${certId}/certificate`); + }>(`/api/v1/pki/certificates/${serialNumber}/certificate`); return data; }, - enabled: Boolean(certId) + enabled: Boolean(serialNumber) }); }; diff --git a/frontend/src/hooks/api/certificates/types.ts b/frontend/src/hooks/api/certificates/types.ts index 71f2e5cd9..0ea0d6323 100644 --- a/frontend/src/hooks/api/certificates/types.ts +++ b/frontend/src/hooks/api/certificates/types.ts @@ -1,7 +1,21 @@ +import { CertStatus } from "./enums"; + export type TCertificate = { id: string; caId: string; + status: CertStatus; commonName: string; + serialNumber: string; notBefore: string; notAfter: string; }; + +export type TDeleteCertDTO = { + projectSlug: string; + serialNumber: string; +}; + +export type TRevokeCertDTO = { + projectSlug: string; + serialNumber: string; +}; diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateCertModal.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateCertModal.tsx index 6ea79d180..97c56b0ec 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateCertModal.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateCertModal.tsx @@ -11,7 +11,7 @@ type Props = { export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => { const { data } = useGetCertCert( - (popUp?.certificateCert?.data as { certId: string })?.certId || "" + (popUp?.certificateCert?.data as { serialNumber: string })?.serialNumber || "" ); return ( diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateContent.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateContent.tsx index ac5eb3fd7..5b3773212 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateContent.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateContent.tsx @@ -111,41 +111,45 @@ export const CertificateContent = ({

{certificate}

-
-

Certificate Chain

-
- { - navigator.clipboard.writeText(certificateChain); - setIsCertificateChainCopied.on(); - }} - > - - - Copy - - - { - downloadTxtFile("certificate_chain.txt", certificate); - }} - > - - - Download - - -
-
-
-

{certificateChain}

-
+ {certificateChain && ( + <> +
+

Certificate Chain

+
+ { + navigator.clipboard.writeText(certificateChain); + setIsCertificateChainCopied.on(); + }} + > + + + Copy + + + { + downloadTxtFile("certificate_chain.txt", certificateChain); + }} + > + + + Download + + +
+
+
+

{certificateChain}

+
+ + )} {privateKey && ( <>
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 6fd27d145..fe91ca603 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx @@ -15,12 +15,7 @@ import { SelectItem } from "@app/components/v2"; import { useWorkspace } from "@app/context"; -import { - CaStatus, - useCreateCertificate, - useGetCertById, - useListWorkspaceCas -} from "@app/hooks/api"; +import { CaStatus, useCreateCertificate, useGetCert, useListWorkspaceCas } from "@app/hooks/api"; import { caTypeToNameMap } from "@app/hooks/api/ca/constants"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -60,8 +55,8 @@ type TCertificateDetails = { export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { const [certificateDetails, setCertificateDetails] = useState(null); const { currentWorkspace } = useWorkspace(); - const { data: cert } = useGetCertById( - (popUp?.certificate?.data as { certId: string })?.certId || "" + const { data: cert } = useGetCert( + (popUp?.certificate?.data as { serialNumber: string })?.serialNumber || "" ); const { data: cas } = useListWorkspaceCas({ diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesSection.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesSection.tsx index addcff7a8..cf6a338c2 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesSection.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesSection.tsx @@ -2,9 +2,10 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; -import { useDeleteCert } from "@app/hooks/api"; +import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { useDeleteCert, useRevokeCert } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; import { CertificateCertModal } from "./CertificateCertModal"; @@ -14,18 +15,20 @@ import { CertificatesTable } from "./CertificatesTable"; export const CertificatesSection = () => { const { currentWorkspace } = useWorkspace(); const { mutateAsync: deleteCert } = useDeleteCert(); + const { mutateAsync: revokeCert } = useRevokeCert(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "certificate", "certificateCert", - "deleteCertificate" + "deleteCertificate", + "revokeCertificate" ] as const); - const onRemoveCertificateSubmit = async (certId: string) => { + const onRemoveCertificateSubmit = async (serialNumber: string) => { try { if (!currentWorkspace?.slug) return; - await deleteCert({ certId, projectSlug: currentWorkspace.slug }); + await deleteCert({ serialNumber, projectSlug: currentWorkspace.slug }); await createNotification({ text: "Successfully deleted certificate", @@ -42,23 +45,47 @@ export const CertificatesSection = () => { } }; + const onRevokeCertificateSubmit = async (serialNumber: string) => { + try { + if (!currentWorkspace?.slug) return; + + await revokeCert({ serialNumber, projectSlug: currentWorkspace.slug }); + + await createNotification({ + text: "Successfully revoked certificate", + type: "success" + }); + + handlePopUpClose("revokeCertificate"); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to revoke certificate", + type: "error" + }); + } + }; + return (

Certificates

- {/* - {(isAllowed) => ( */} - - {/* )} */} - {/* */} + {(isAllowed) => ( + + )} +
@@ -71,7 +98,23 @@ export const CertificatesSection = () => { onChange={(isOpen) => handlePopUpToggle("deleteCertificate", isOpen)} deleteKey="confirm" onDeleteApproved={() => - onRemoveCertificateSubmit((popUp?.deleteCertificate?.data as { certId: string })?.certId) + onRemoveCertificateSubmit( + (popUp?.deleteCertificate?.data as { serialNumber: string })?.serialNumber + ) + } + /> + handlePopUpToggle("revokeCertificate", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onRevokeCertificateSubmit( + (popUp?.revokeCertificate?.data as { serialNumber: string }).serialNumber + ) } />
diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesTable.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesTable.tsx index 663ac6f8b..db02f2386 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesTable.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesTable.tsx @@ -1,4 +1,5 @@ import { + faBan, faCertificate, faEllipsis, faEye, @@ -24,16 +25,20 @@ import { Th, THead, Tooltip, - Tr} from "@app/components/v2"; + Tr +} from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { useListWorkspaceCertificates } from "@app/hooks/api"; +import { certStatusToNameMap } from "@app/hooks/api/certificates/constants"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { handlePopUpOpen: ( - popUpName: keyof UsePopUpState<["certificate", "deleteCertificate", "certificateCert"]>, + popUpName: keyof UsePopUpState< + ["certificate", "deleteCertificate", "revokeCertificate", "certificateCert"] + >, data?: { - certId?: string; + serialNumber?: string; commonName?: string; } ) => void; @@ -48,8 +53,8 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { - + @@ -62,8 +67,8 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { data.map((certificate) => { return ( - +
Certificate ID Common NameStatus Valid Until
{certificate.id} {certificate.commonName}{certStatusToNameMap[certificate.status]} {certificate.notAfter ? format(new Date(certificate.notAfter), "yyyy-MM-dd") @@ -90,7 +95,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { )} onClick={async () => handlePopUpOpen("certificateCert", { - certId: certificate.id + serialNumber: certificate.serialNumber }) } disabled={!isAllowed} @@ -111,7 +116,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { )} onClick={async () => handlePopUpOpen("certificate", { - certId: certificate.id + serialNumber: certificate.serialNumber }) } disabled={!isAllowed} @@ -121,6 +126,27 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { )} + + {(isAllowed) => ( + + handlePopUpOpen("revokeCertificate", { + serialNumber: certificate.serialNumber + }) + } + disabled={!isAllowed} + icon={} + > + Revoke Certificate + + )} + { )} onClick={async () => handlePopUpOpen("deleteCertificate", { - certId: certificate.id, + serialNumber: certificate.serialNumber, commonName: certificate.commonName }) }