From e10e313af36969ddd8a3b371baa326d95493a143 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 9 Sep 2024 12:42:56 -0700 Subject: [PATCH] Finish cert template enforcement --- ...0240909145938_cert-template-enforcement.ts | 25 ++++++ .../src/db/schemas/certificate-authorities.ts | 3 +- backend/src/db/schemas/secret-sharing.ts | 4 +- .../ee/services/audit-log/audit-log-types.ts | 10 +++ backend/src/lib/api-docs/constants.ts | 8 +- .../routes/v1/certificate-authority-router.ts | 59 ++++++++++++- .../server/routes/v1/certificate-router.ts | 4 +- .../certificate-authority-service.ts | 62 ++++++++++++-- .../certificate-authority-types.ts | 6 ++ frontend/package-lock.json | 1 - frontend/src/hooks/api/ca/index.tsx | 2 +- frontend/src/hooks/api/ca/mutations.tsx | 3 +- frontend/src/hooks/api/ca/queries.tsx | 15 ++++ frontend/src/hooks/api/ca/types.ts | 3 + .../api/certificateTemplates/mutations.tsx | 15 ++-- frontend/src/views/Project/CaPage/CaPage.tsx | 2 + .../CaPage/components/CaDetailsSection.tsx | 30 ++++++- .../components/CaTab/components/CaModal.tsx | 83 +++++++++++++------ .../components/CaTab/components/CaTable.tsx | 23 ----- .../CertificatesTab/CertificatesTab.tsx | 4 +- .../components/CertificateTemplateModal.tsx | 20 +++-- .../CertificateTemplatesSection.tsx | 41 +++++++-- .../components/CertificateTemplatesTable.tsx | 27 +++--- 23 files changed, 353 insertions(+), 97 deletions(-) create mode 100644 backend/src/db/migrations/20240909145938_cert-template-enforcement.ts diff --git a/backend/src/db/migrations/20240909145938_cert-template-enforcement.ts b/backend/src/db/migrations/20240909145938_cert-template-enforcement.ts new file mode 100644 index 000000000..4bf08ad76 --- /dev/null +++ b/backend/src/db/migrations/20240909145938_cert-template-enforcement.ts @@ -0,0 +1,25 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.CertificateAuthority)) { + const hasTemplateIssuanceRequiredColumn = await knex.schema.hasColumn( + TableName.CertificateAuthority, + "templateIssuanceRequired" + ); + if (!hasTemplateIssuanceRequiredColumn) { + await knex.schema.alterTable(TableName.CertificateAuthority, (t) => { + t.boolean("requireTemplateForIssuance").notNullable().defaultTo(false); + }); + } + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.CertificateAuthority)) { + await knex.schema.alterTable(TableName.CertificateAuthority, (t) => { + t.dropColumn("requireTemplateForIssuance"); + }); + } +} diff --git a/backend/src/db/schemas/certificate-authorities.ts b/backend/src/db/schemas/certificate-authorities.ts index e59a9225c..ffe0f7c44 100644 --- a/backend/src/db/schemas/certificate-authorities.ts +++ b/backend/src/db/schemas/certificate-authorities.ts @@ -28,7 +28,8 @@ export const CertificateAuthoritiesSchema = z.object({ keyAlgorithm: z.string(), notBefore: z.date().nullable().optional(), notAfter: z.date().nullable().optional(), - activeCaCertId: z.string().uuid().nullable().optional() + activeCaCertId: z.string().uuid().nullable().optional(), + requireTemplateForIssuance: z.boolean().default(false) }); export type TCertificateAuthorities = z.infer; diff --git a/backend/src/db/schemas/secret-sharing.ts b/backend/src/db/schemas/secret-sharing.ts index be5643e2b..2d0fc5eb5 100644 --- a/backend/src/db/schemas/secret-sharing.ts +++ b/backend/src/db/schemas/secret-sharing.ts @@ -21,8 +21,8 @@ export const SecretSharingSchema = z.object({ expiresAfterViews: z.number().nullable().optional(), accessType: z.string().default("anyone"), name: z.string().nullable().optional(), - password: z.string().nullable().optional(), - lastViewedAt: z.date().nullable().optional() + lastViewedAt: z.date().nullable().optional(), + password: z.string().nullable().optional() }); export type TSecretSharing = z.infer; diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 981b3777e..0496cf984 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -140,6 +140,7 @@ export enum EventType { GET_CA_CRLS = "get-certificate-authority-crls", ISSUE_CERT = "issue-cert", SIGN_CERT = "sign-cert", + GET_CA_CERTIFICATE_TEMPLATES = "get-ca-certificate-templates", GET_CERT = "get-cert", DELETE_CERT = "delete-cert", REVOKE_CERT = "revoke-cert", @@ -1192,6 +1193,14 @@ interface SignCert { }; } +interface GetCaCertificateTemplates { + type: EventType.GET_CA_CERTIFICATE_TEMPLATES; + metadata: { + caId: string; + dn: string; + }; +} + interface GetCert { type: EventType.GET_CERT; metadata: { @@ -1547,6 +1556,7 @@ export type Event = | GetCaCrls | IssueCert | SignCert + | GetCaCertificateTemplates | GetCert | DeleteCert | RevokeCert diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index d38620837..b08c70d93 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1037,14 +1037,18 @@ export const CERTIFICATE_AUTHORITIES = { maxPathLength: "The maximum number of intermediate CAs that may follow this CA in the certificate / CA chain. A maxPathLength of -1 implies no path limit on the chain.", keyAlgorithm: - "The type of public key algorithm and size, in bits, of the key pair for the CA; when you create an intermediate CA, you must use a key algorithm supported by the parent CA." + "The type of public key algorithm and size, in bits, of the key pair for the CA; when you create an intermediate CA, you must use a key algorithm supported by the parent CA.", + requireTemplateForIssuance: + "Whether or not certificates for this CA can only be issued through certificate templates." }, GET: { caId: "The ID of the CA to get" }, UPDATE: { caId: "The ID of the CA to update", - status: "The status of the CA to update to. This can be one of active or disabled" + status: "The status of the CA to update to. This can be one of active or disabled", + requireTemplateForIssuance: + "Whether or not certificates for this CA can only be issued through certificate templates." }, DELETE: { caId: "The ID of the CA to delete" diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index 9a866f66e..3c14b851e 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -1,7 +1,7 @@ import ms from "ms"; import { z } from "zod"; -import { CertificateAuthoritiesSchema } from "@app/db/schemas"; +import { CertificateAuthoritiesSchema, CertificateTemplatesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -42,7 +42,11 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { keyAlgorithm: z .nativeEnum(CertKeyAlgorithm) .default(CertKeyAlgorithm.RSA_2048) - .describe(CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm) + .describe(CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm), + requireTemplateForIssuance: z + .boolean() + .default(true) + .describe(CERTIFICATE_AUTHORITIES.CREATE.requireTemplateForIssuance) }) .refine( (data) => { @@ -148,7 +152,11 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.UPDATE.caId) }), body: z.object({ - status: z.enum([CaStatus.ACTIVE, CaStatus.DISABLED]).optional().describe(CERTIFICATE_AUTHORITIES.UPDATE.status) + status: z.enum([CaStatus.ACTIVE, CaStatus.DISABLED]).optional().describe(CERTIFICATE_AUTHORITIES.UPDATE.status), + requireTemplateForIssuance: z + .boolean() + .optional() + .describe(CERTIFICATE_AUTHORITIES.CREATE.requireTemplateForIssuance) }), response: { 200: z.object({ @@ -700,6 +708,51 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/:caId/certificate-templates", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Get list of certificate templates for the CA", + params: z.object({ + caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId) + }), + response: { + 200: z.object({ + certificateTemplates: CertificateTemplatesSchema.array() + }) + } + }, + handler: async (req) => { + const { certificateTemplates, ca } = await server.services.certificateAuthority.getCaCertificateTemplates({ + caId: req.params.caId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.GET_CA_CERTIFICATE_TEMPLATES, + metadata: { + caId: ca.id, + dn: ca.dn + } + } + }); + + return { + certificateTemplates + }; + } + }); + server.route({ method: "GET", url: "/:caId/crls", diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index 91ae85982..2558b4970 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -101,7 +101,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { .refine( (data) => (data.caId !== undefined && data.certificateTemplateId === undefined) || - (data.caId === undefined && data.certificateTemplateId !== undefined), + (data.caId === undefined && data.pkiCollectionId === undefined && data.certificateTemplateId !== undefined), { message: "Either CA ID or Certificate Template ID must be present, but not both", path: ["caId", "certificateTemplateId"] @@ -192,7 +192,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { .refine( (data) => (data.caId !== undefined && data.certificateTemplateId === undefined) || - (data.caId === undefined && data.certificateTemplateId !== undefined), + (data.caId === undefined && data.pkiCollectionId === undefined && data.certificateTemplateId !== undefined), { message: "Either CA ID or Certificate Template ID must be present, but not both", path: ["caId", "certificateTemplateId"] diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index bfd7489f7..1c2a5a689 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -41,6 +41,7 @@ import { TCreateCaDTO, TDeleteCaDTO, TGetCaCertDTO, + TGetCaCertificateTemplatesDTO, TGetCaCertsDTO, TGetCaCsrDTO, TGetCaDTO, @@ -64,7 +65,7 @@ type TCertificateAuthorityServiceFactoryDep = { >; certificateAuthoritySecretDAL: Pick; certificateAuthorityCrlDAL: Pick; - certificateTemplateDAL: Pick; + certificateTemplateDAL: Pick; certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick certificateDAL: Pick; certificateBodyDAL: Pick; @@ -108,6 +109,7 @@ export const certificateAuthorityServiceFactory = ({ notAfter, maxPathLength, keyAlgorithm, + requireTemplateForIssuance, actorId, actorAuthMethod, actor, @@ -170,7 +172,8 @@ export const certificateAuthorityServiceFactory = ({ notBefore: notBeforeDate, notAfter: notAfterDate, serialNumber - }) + }), + requireTemplateForIssuance }, tx ); @@ -302,7 +305,15 @@ export const certificateAuthorityServiceFactory = ({ * Update CA with id [caId]. * Note: Used to enable/disable CA */ - const updateCaById = async ({ caId, status, actorId, actorAuthMethod, actor, actorOrgId }: TUpdateCaDTO) => { + const updateCaById = async ({ + caId, + status, + requireTemplateForIssuance, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TUpdateCaDTO) => { const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new BadRequestError({ message: "CA not found" }); @@ -319,7 +330,7 @@ export const certificateAuthorityServiceFactory = ({ ProjectPermissionSub.CertificateAuthorities ); - const updatedCa = await certificateAuthorityDAL.updateById(caId, { status }); + const updatedCa = await certificateAuthorityDAL.updateById(caId, { status, requireTemplateForIssuance }); return updatedCa; }; @@ -1077,6 +1088,9 @@ export const certificateAuthorityServiceFactory = ({ if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + if (ca.requireTemplateForIssuance && !certificateTemplate) { + throw new BadRequestError({ message: "Certificate template is required for issuance" }); + } const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); if (ca.notAfter && new Date() > new Date(ca.notAfter)) { @@ -1347,6 +1361,9 @@ export const certificateAuthorityServiceFactory = ({ if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + if (ca.requireTemplateForIssuance && !certificateTemplate) { + throw new BadRequestError({ message: "Certificate template is required for issuance" }); + } const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); @@ -1568,6 +1585,40 @@ export const certificateAuthorityServiceFactory = ({ }; }; + /** + * Return list of certificate templates for CA with id [caId]. + */ + const getCaCertificateTemplates = async ({ + caId, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TGetCaCertificateTemplatesDTO) => { + const ca = await certificateAuthorityDAL.findById(caId); + if (!ca) throw new BadRequestError({ message: "CA not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + ca.projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.CertificateTemplates + ); + + const certificateTemplates = await certificateTemplateDAL.find({ caId }); + + return { + certificateTemplates, + ca + }; + }; + return { createCa, getCaById, @@ -1580,6 +1631,7 @@ export const certificateAuthorityServiceFactory = ({ signIntermediate, importCertToCa, issueCertFromCa, - signCertFromCa + signCertFromCa, + getCaCertificateTemplates }; }; diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index 764a5dca9..5876c0057 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -38,6 +38,7 @@ export type TCreateCaDTO = { notAfter?: string; maxPathLength: number; keyAlgorithm: CertKeyAlgorithm; + requireTemplateForIssuance: boolean; } & Omit; export type TGetCaDTO = { @@ -47,6 +48,7 @@ export type TGetCaDTO = { export type TUpdateCaDTO = { caId: string; status?: CaStatus; + requireTemplateForIssuance?: boolean; } & Omit; export type TDeleteCaDTO = { @@ -125,6 +127,10 @@ export type TSignCertFromCaDTO = notAfter?: string; } & Omit); +export type TGetCaCertificateTemplatesDTO = { + caId: string; +} & Omit; + export type TDNParts = { commonName?: string; organization?: string; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3148f5da2..95104cdf1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "frontend", "dependencies": { "@casl/ability": "^6.5.0", "@casl/react": "^3.1.0", diff --git a/frontend/src/hooks/api/ca/index.tsx b/frontend/src/hooks/api/ca/index.tsx index 5c0ff4caa..4993a8fdc 100644 --- a/frontend/src/hooks/api/ca/index.tsx +++ b/frontend/src/hooks/api/ca/index.tsx @@ -8,4 +8,4 @@ export { useSignIntermediate, useUpdateCa } from "./mutations"; -export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrls, useGetCaCsr } from "./queries"; +export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCertTemplates,useGetCaCrls, useGetCaCsr } from "./queries"; diff --git a/frontend/src/hooks/api/ca/mutations.tsx b/frontend/src/hooks/api/ca/mutations.tsx index 48652f3fd..5e668ecef 100644 --- a/frontend/src/hooks/api/ca/mutations.tsx +++ b/frontend/src/hooks/api/ca/mutations.tsx @@ -43,8 +43,9 @@ export const useUpdateCa = () => { } = await apiRequest.patch<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`, body); return ca; }, - onSuccess: (_, { projectSlug }) => { + onSuccess: ({ id }, { projectSlug }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug })); + queryClient.invalidateQueries(caKeys.getCaById(id)); } }); }; diff --git a/frontend/src/hooks/api/ca/queries.tsx b/frontend/src/hooks/api/ca/queries.tsx index 996043f19..a1e633776 100644 --- a/frontend/src/hooks/api/ca/queries.tsx +++ b/frontend/src/hooks/api/ca/queries.tsx @@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { TCertificateTemplate } from "../certificateTemplates/types"; import { TCertificateAuthority } from "./types"; export const caKeys = { @@ -11,6 +12,7 @@ export const caKeys = { getCaCert: (caId: string) => [{ caId }, "ca-cert"], getCaCsr: (caId: string) => [{ caId }, "ca-csr"], getCaCrl: (caId: string) => [{ caId }, "ca-crl"], + getCaCertTemplates: (caId: string) => [{ caId }, "ca-cert-templates"], getCaEstConfig: (caId: string) => [{ caId }, "ca-est-config"] }; @@ -90,3 +92,16 @@ export const useGetCaCrls = (caId: string) => { enabled: Boolean(caId) }); }; + +export const useGetCaCertTemplates = (caId: string) => { + return useQuery({ + queryKey: caKeys.getCaCertTemplates(caId), + queryFn: async () => { + const { data } = await apiRequest.get<{ + certificateTemplates: TCertificateTemplate[]; + }>(`/api/v1/pki/ca/${caId}/certificate-templates`); + return data; + }, + enabled: Boolean(caId) + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts index e09ae16b8..52e363fa7 100644 --- a/frontend/src/hooks/api/ca/types.ts +++ b/frontend/src/hooks/api/ca/types.ts @@ -19,6 +19,7 @@ export type TCertificateAuthority = { notAfter?: string; notBefore?: string; keyAlgorithm: CertKeyAlgorithm; + requireTemplateForIssuance: boolean; activeCaCertId?: string; createdAt: string; updatedAt: string; @@ -37,12 +38,14 @@ export type TCreateCaDTO = { notAfter?: string; maxPathLength: number; keyAlgorithm: CertKeyAlgorithm; + requireTemplateForIssuance: boolean; }; export type TUpdateCaDTO = { projectSlug: string; caId: string; status?: CaStatus; + requireTemplateForIssuance?: boolean; }; export type TDeleteCaDTO = { diff --git a/frontend/src/hooks/api/certificateTemplates/mutations.tsx b/frontend/src/hooks/api/certificateTemplates/mutations.tsx index 101507af0..f633c0332 100644 --- a/frontend/src/hooks/api/certificateTemplates/mutations.tsx +++ b/frontend/src/hooks/api/certificateTemplates/mutations.tsx @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { caKeys } from "../ca/queries"; import { workspaceKeys } from "../workspace/queries"; import { certTemplateKeys } from "./queries"; import { @@ -23,8 +24,9 @@ export const useCreateCertTemplate = () => { ); return certificateTemplate; }, - onSuccess: (_, { projectId }) => { + onSuccess: ({ caId }, { projectId }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId)); + queryClient.invalidateQueries(caKeys.getCaCertTemplates(caId)); } }); }; @@ -40,22 +42,25 @@ export const useUpdateCertTemplate = () => { return certificateTemplate; }, - onSuccess: (_, { projectId, id }) => { + onSuccess: ({ caId }, { projectId, id }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId)); queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id)); + queryClient.invalidateQueries(caKeys.getCaCertTemplates(caId)); } }); }; export const useDeleteCertTemplate = () => { const queryClient = useQueryClient(); - return useMutation({ + return useMutation({ mutationFn: async (data) => { - return apiRequest.delete(`/api/v1/pki/certificate-templates/${data.id}`); + const { data: certificateTemplate } = await apiRequest.delete(`/api/v1/pki/certificate-templates/${data.id}`); + return certificateTemplate; }, - onSuccess: (_, { projectId, id }) => { + onSuccess: ({ caId }, { projectId, id }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId)); queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id)); + queryClient.invalidateQueries(caKeys.getCaCertTemplates(caId)); } }); }; diff --git a/frontend/src/views/Project/CaPage/CaPage.tsx b/frontend/src/views/Project/CaPage/CaPage.tsx index 23f94570c..4afb537f0 100644 --- a/frontend/src/views/Project/CaPage/CaPage.tsx +++ b/frontend/src/views/Project/CaPage/CaPage.tsx @@ -22,6 +22,7 @@ import { usePopUp } from "@app/hooks/usePopUp"; import { CaModal } from "@app/views/Project/CertificatesPage/components/CaTab/components/CaModal"; import { CaInstallCertModal } from "../CertificatesPage/components/CaTab/components/CaInstallCertModal"; +import { CertificateTemplatesSection } from "../CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection"; import { CaCertificatesSection, CaCrlsSection, @@ -125,6 +126,7 @@ export const CaPage = withProjectPermission(
+
diff --git a/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx b/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx index 94eae3e6b..4bff13225 100644 --- a/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx +++ b/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx @@ -1,4 +1,4 @@ -import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { format } from "date-fns"; @@ -33,6 +33,28 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {

CA Details

+ + {(isAllowed) => { + return ( + + { + e.stopPropagation(); + handlePopUpOpen("ca", { + caId: ca.id + }); + }} + > + + + + ); + }} +
@@ -115,6 +137,12 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => { {ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"}

+
+

Template Issuance Required

+

+ {ca.requireTemplateForIssuance ? "True" : "False"} +

+
{ca.status === CaStatus.ACTIVE && ( { // const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false); const { data: ca } = useGetCaById((popUp?.ca?.data as { caId: string })?.caId || ""); + const { mutateAsync: createMutateAsync } = useCreateCa(); + const { mutateAsync: updateMutateAsync } = useUpdateCa(); const { control, @@ -110,7 +114,8 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { commonName: ca.commonName, notAfter: ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "", maxPathLength: ca.maxPathLength ? String(ca.maxPathLength) : "", - keyAlgorithm: ca.keyAlgorithm + keyAlgorithm: ca.keyAlgorithm, + requireTemplateForIssuance: ca.requireTemplateForIssuance }); } else { reset({ @@ -124,7 +129,8 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { commonName: "", notAfter: getDateTenYearsFromToday(), maxPathLength: "-1", - keyAlgorithm: CertKeyAlgorithm.RSA_2048 + keyAlgorithm: CertKeyAlgorithm.RSA_2048, + requireTemplateForIssuance: true }); } }, [ca]); @@ -140,31 +146,43 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { province, notAfter, maxPathLength, - keyAlgorithm + keyAlgorithm, + requireTemplateForIssuance }: FormData) => { try { if (!currentWorkspace?.slug) return; - - await createMutateAsync({ - projectSlug: currentWorkspace.slug, - type, - friendlyName, - commonName, - organization, - ou, - country, - province, - locality, - notAfter, - maxPathLength: Number(maxPathLength), - keyAlgorithm - }); + + if (ca) { + // update + await updateMutateAsync({ + projectSlug: currentWorkspace.slug, + caId: ca.id, + requireTemplateForIssuance + }); + } else { + // create + await createMutateAsync({ + projectSlug: currentWorkspace.slug, + type, + friendlyName, + commonName, + organization, + ou, + country, + province, + locality, + notAfter, + maxPathLength: Number(maxPathLength), + keyAlgorithm, + requireTemplateForIssuance + }); + } reset(); handlePopUpToggle("ca", false); createNotification({ - text: "Successfully created CA", + text: `Successfully ${ca ? "updated" : "created"} CA`, type: "success" }); } catch (err) { @@ -406,7 +424,24 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { )} /> - {!ca && ( + { + return ( + + field.onChange(value)} + isChecked={field.value} + > +

Require Template for Certificate Issuance

+
+
+ ); + }} + /> + {/* {!ca && ( */}
- )} + {/* )} */} diff --git a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx index d4e7e957d..35c034f34 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx @@ -3,7 +3,6 @@ import { faBan, faCertificate, faEllipsis, - faEye, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -155,28 +154,6 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { )}
)} - - {(isAllowed) => ( - { - e.stopPropagation(); - handlePopUpOpen("ca", { - caId: ca.id - }); - }} - disabled={!isAllowed} - icon={} - > - View CA - - )} - {(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && ( { @@ -14,7 +14,7 @@ export const CertificatesTab = () => { exit={{ opacity: 0, translateX: 30 }} > - + {/* */} ); diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplateModal.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplateModal.tsx index 3666627e5..8074cbaf5 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplateModal.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplateModal.tsx @@ -21,11 +21,11 @@ import { useWorkspace } from "@app/context"; import { CaStatus, useCreateCertTemplate, + useGetCaById, useGetCertTemplate, useListWorkspaceCas, useListWorkspacePkiCollections, - useUpdateCertTemplate -} from "@app/hooks/api"; + useUpdateCertTemplate} from "@app/hooks/api"; import { caTypeToNameMap } from "@app/hooks/api/ca/constants"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -51,6 +51,7 @@ const schema = z.object({ export type FormData = z.infer; type Props = { + caId: string; popUp: UsePopUpState<["certificateTemplate"]>; handlePopUpToggle: ( popUpName: keyof UsePopUpState<["certificateTemplate"]>, @@ -58,8 +59,11 @@ type Props = { ) => void; }; -export const CertificateTemplateModal = ({ popUp, handlePopUpToggle }: Props) => { +export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Props) => { const { currentWorkspace } = useWorkspace(); + + const { data: ca } = useGetCaById(caId); + const { data: certTemplate } = useGetCertTemplate( (popUp?.certificateTemplate?.data as { id: string })?.id || "" ); @@ -97,16 +101,15 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle }: Props) => }); } else { reset({ - caId: "", + caId, name: "", commonName: "", ttl: "" }); } - }, [certTemplate]); + }, [certTemplate, ca]); const onFormSubmit = async ({ - caId, collectionId, name, commonName, @@ -129,6 +132,8 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle }: Props) => subjectAlternativeName, ttl }); + + // TODO: requireTemplateForIssuance field createNotification({ text: "Successfully updated certificate template", @@ -190,7 +195,7 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle }: Props) => ( {...field} onValueChange={(e) => onChange(e)} className="w-full" + isDisabled > {(cas || []).map(({ id, type, dn }) => ( diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx index 4c3ca73fa..e76d2ce33 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx @@ -1,9 +1,13 @@ +/** + * TODO (dangtony98): Reevaluate if this component should be in main + * CertificateTab or under CA page in the future. + */ 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, UpgradePlanModal } from "@app/components/v2"; +import { DeleteActionModal, IconButton, UpgradePlanModal } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useDeleteCertTemplate } from "@app/hooks/api"; @@ -12,7 +16,11 @@ import { CertificateTemplateEnrollmentModal } from "./CertificateTemplateEnrollm import { CertificateTemplateModal } from "./CertificateTemplateModal"; import { CertificateTemplatesTable } from "./CertificateTemplatesTable"; -export const CertificateTemplatesSection = () => { +type Props = { + caId: string; +} + +export const CertificateTemplatesSection = ({ caId }: Props) => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "certificateTemplate", "deleteCertificateTemplate", @@ -50,8 +58,8 @@ export const CertificateTemplatesSection = () => { }; return ( -
-
+
+ {/*

Certificate Templates

{ )} +
*/} +
+

Certificate Templates

+ + {(isAllowed) => ( + handlePopUpOpen("certificateTemplate")} + isDisabled={!isAllowed} + > + + + )} +
- - +
+ +
+ void; }; -export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => { - const { currentWorkspace } = useWorkspace(); +export const CertificateTemplatesTable = ({ handlePopUpOpen, caId }: Props) => { const { subscription } = useSubscription(); - const { data, isLoading } = useListWorkspaceCertificateTemplates({ - workspaceId: currentWorkspace?.id ?? "" - }); + + const { data, isLoading } = useGetCaCertTemplates(caId); + + // const { data, isLoading } = useListWorkspaceCertificateTemplates({ + // workspaceId: currentWorkspace?.id ?? "" + // }); return (
@@ -54,7 +59,7 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => { Name - Certificate Authority + {/* Certificate Authority */} @@ -65,13 +70,13 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => { return ( {certificateTemplate.name} - {certificateTemplate.caName} + {/* {certificateTemplate.caName} */}
- +
@@ -143,7 +148,7 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => { {!isLoading && !data?.certificateTemplates?.length && ( - + )}