Finish cert template enforcement

This commit is contained in:
Tuan Dang
2024-09-09 12:42:56 -07:00
parent 4baa6b1d3d
commit e10e313af3
23 changed files with 353 additions and 97 deletions
@@ -0,0 +1,25 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
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<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.dropColumn("requireTemplateForIssuance");
});
}
}
@@ -28,7 +28,8 @@ export const CertificateAuthoritiesSchema = z.object({
keyAlgorithm: z.string(), keyAlgorithm: z.string(),
notBefore: z.date().nullable().optional(), notBefore: z.date().nullable().optional(),
notAfter: 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<typeof CertificateAuthoritiesSchema>; export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>;
+2 -2
View File
@@ -21,8 +21,8 @@ export const SecretSharingSchema = z.object({
expiresAfterViews: z.number().nullable().optional(), expiresAfterViews: z.number().nullable().optional(),
accessType: z.string().default("anyone"), accessType: z.string().default("anyone"),
name: z.string().nullable().optional(), 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<typeof SecretSharingSchema>; export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
@@ -140,6 +140,7 @@ export enum EventType {
GET_CA_CRLS = "get-certificate-authority-crls", GET_CA_CRLS = "get-certificate-authority-crls",
ISSUE_CERT = "issue-cert", ISSUE_CERT = "issue-cert",
SIGN_CERT = "sign-cert", SIGN_CERT = "sign-cert",
GET_CA_CERTIFICATE_TEMPLATES = "get-ca-certificate-templates",
GET_CERT = "get-cert", GET_CERT = "get-cert",
DELETE_CERT = "delete-cert", DELETE_CERT = "delete-cert",
REVOKE_CERT = "revoke-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 { interface GetCert {
type: EventType.GET_CERT; type: EventType.GET_CERT;
metadata: { metadata: {
@@ -1547,6 +1556,7 @@ export type Event =
| GetCaCrls | GetCaCrls
| IssueCert | IssueCert
| SignCert | SignCert
| GetCaCertificateTemplates
| GetCert | GetCert
| DeleteCert | DeleteCert
| RevokeCert | RevokeCert
+6 -2
View File
@@ -1037,14 +1037,18 @@ export const CERTIFICATE_AUTHORITIES = {
maxPathLength: 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.", "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: 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: { GET: {
caId: "The ID of the CA to get" caId: "The ID of the CA to get"
}, },
UPDATE: { UPDATE: {
caId: "The ID of the CA to 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: { DELETE: {
caId: "The ID of the CA to delete" caId: "The ID of the CA to delete"
@@ -1,7 +1,7 @@
import ms from "ms"; import ms from "ms";
import { z } from "zod"; 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 { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs"; import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -42,7 +42,11 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
keyAlgorithm: z keyAlgorithm: z
.nativeEnum(CertKeyAlgorithm) .nativeEnum(CertKeyAlgorithm)
.default(CertKeyAlgorithm.RSA_2048) .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( .refine(
(data) => { (data) => {
@@ -148,7 +152,11 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.UPDATE.caId) caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.UPDATE.caId)
}), }),
body: z.object({ 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: { response: {
200: z.object({ 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({ server.route({
method: "GET", method: "GET",
url: "/:caId/crls", url: "/:caId/crls",
@@ -101,7 +101,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
.refine( .refine(
(data) => (data) =>
(data.caId !== undefined && data.certificateTemplateId === undefined) || (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", message: "Either CA ID or Certificate Template ID must be present, but not both",
path: ["caId", "certificateTemplateId"] path: ["caId", "certificateTemplateId"]
@@ -192,7 +192,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
.refine( .refine(
(data) => (data) =>
(data.caId !== undefined && data.certificateTemplateId === undefined) || (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", message: "Either CA ID or Certificate Template ID must be present, but not both",
path: ["caId", "certificateTemplateId"] path: ["caId", "certificateTemplateId"]
@@ -41,6 +41,7 @@ import {
TCreateCaDTO, TCreateCaDTO,
TDeleteCaDTO, TDeleteCaDTO,
TGetCaCertDTO, TGetCaCertDTO,
TGetCaCertificateTemplatesDTO,
TGetCaCertsDTO, TGetCaCertsDTO,
TGetCaCsrDTO, TGetCaCsrDTO,
TGetCaDTO, TGetCaDTO,
@@ -64,7 +65,7 @@ type TCertificateAuthorityServiceFactoryDep = {
>; >;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">; certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">; certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">;
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById">; certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById" | "find">;
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">; certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">; certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
@@ -108,6 +109,7 @@ export const certificateAuthorityServiceFactory = ({
notAfter, notAfter,
maxPathLength, maxPathLength,
keyAlgorithm, keyAlgorithm,
requireTemplateForIssuance,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actor, actor,
@@ -170,7 +172,8 @@ export const certificateAuthorityServiceFactory = ({
notBefore: notBeforeDate, notBefore: notBeforeDate,
notAfter: notAfterDate, notAfter: notAfterDate,
serialNumber serialNumber
}) }),
requireTemplateForIssuance
}, },
tx tx
); );
@@ -302,7 +305,15 @@ export const certificateAuthorityServiceFactory = ({
* Update CA with id [caId]. * Update CA with id [caId].
* Note: Used to enable/disable CA * 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); const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" }); if (!ca) throw new BadRequestError({ message: "CA not found" });
@@ -319,7 +330,7 @@ export const certificateAuthorityServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities ProjectPermissionSub.CertificateAuthorities
); );
const updatedCa = await certificateAuthorityDAL.updateById(caId, { status }); const updatedCa = await certificateAuthorityDAL.updateById(caId, { status, requireTemplateForIssuance });
return updatedCa; return updatedCa;
}; };
@@ -1077,6 +1088,9 @@ export const certificateAuthorityServiceFactory = ({
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); 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.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); const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
if (ca.notAfter && new Date() > new Date(ca.notAfter)) { 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.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.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); 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 { return {
createCa, createCa,
getCaById, getCaById,
@@ -1580,6 +1631,7 @@ export const certificateAuthorityServiceFactory = ({
signIntermediate, signIntermediate,
importCertToCa, importCertToCa,
issueCertFromCa, issueCertFromCa,
signCertFromCa signCertFromCa,
getCaCertificateTemplates
}; };
}; };
@@ -38,6 +38,7 @@ export type TCreateCaDTO = {
notAfter?: string; notAfter?: string;
maxPathLength: number; maxPathLength: number;
keyAlgorithm: CertKeyAlgorithm; keyAlgorithm: CertKeyAlgorithm;
requireTemplateForIssuance: boolean;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TGetCaDTO = { export type TGetCaDTO = {
@@ -47,6 +48,7 @@ export type TGetCaDTO = {
export type TUpdateCaDTO = { export type TUpdateCaDTO = {
caId: string; caId: string;
status?: CaStatus; status?: CaStatus;
requireTemplateForIssuance?: boolean;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TDeleteCaDTO = { export type TDeleteCaDTO = {
@@ -125,6 +127,10 @@ export type TSignCertFromCaDTO =
notAfter?: string; notAfter?: string;
} & Omit<TProjectPermission, "projectId">); } & Omit<TProjectPermission, "projectId">);
export type TGetCaCertificateTemplatesDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
export type TDNParts = { export type TDNParts = {
commonName?: string; commonName?: string;
organization?: string; organization?: string;
-1
View File
@@ -4,7 +4,6 @@
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "frontend",
"dependencies": { "dependencies": {
"@casl/ability": "^6.5.0", "@casl/ability": "^6.5.0",
"@casl/react": "^3.1.0", "@casl/react": "^3.1.0",
+1 -1
View File
@@ -8,4 +8,4 @@ export {
useSignIntermediate, useSignIntermediate,
useUpdateCa useUpdateCa
} from "./mutations"; } from "./mutations";
export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrls, useGetCaCsr } from "./queries"; export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCertTemplates,useGetCaCrls, useGetCaCsr } from "./queries";
+2 -1
View File
@@ -43,8 +43,9 @@ export const useUpdateCa = () => {
} = await apiRequest.patch<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`, body); } = await apiRequest.patch<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`, body);
return ca; return ca;
}, },
onSuccess: (_, { projectSlug }) => { onSuccess: ({ id }, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug })); queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
queryClient.invalidateQueries(caKeys.getCaById(id));
} }
}); });
}; };
+15
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 { TCertificateTemplate } from "../certificateTemplates/types";
import { TCertificateAuthority } from "./types"; import { TCertificateAuthority } from "./types";
export const caKeys = { export const caKeys = {
@@ -11,6 +12,7 @@ export const caKeys = {
getCaCert: (caId: string) => [{ caId }, "ca-cert"], getCaCert: (caId: string) => [{ caId }, "ca-cert"],
getCaCsr: (caId: string) => [{ caId }, "ca-csr"], getCaCsr: (caId: string) => [{ caId }, "ca-csr"],
getCaCrl: (caId: string) => [{ caId }, "ca-crl"], getCaCrl: (caId: string) => [{ caId }, "ca-crl"],
getCaCertTemplates: (caId: string) => [{ caId }, "ca-cert-templates"],
getCaEstConfig: (caId: string) => [{ caId }, "ca-est-config"] getCaEstConfig: (caId: string) => [{ caId }, "ca-est-config"]
}; };
@@ -90,3 +92,16 @@ export const useGetCaCrls = (caId: string) => {
enabled: Boolean(caId) 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)
});
};
+3
View File
@@ -19,6 +19,7 @@ export type TCertificateAuthority = {
notAfter?: string; notAfter?: string;
notBefore?: string; notBefore?: string;
keyAlgorithm: CertKeyAlgorithm; keyAlgorithm: CertKeyAlgorithm;
requireTemplateForIssuance: boolean;
activeCaCertId?: string; activeCaCertId?: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
@@ -37,12 +38,14 @@ export type TCreateCaDTO = {
notAfter?: string; notAfter?: string;
maxPathLength: number; maxPathLength: number;
keyAlgorithm: CertKeyAlgorithm; keyAlgorithm: CertKeyAlgorithm;
requireTemplateForIssuance: boolean;
}; };
export type TUpdateCaDTO = { export type TUpdateCaDTO = {
projectSlug: string; projectSlug: string;
caId: string; caId: string;
status?: CaStatus; status?: CaStatus;
requireTemplateForIssuance?: boolean;
}; };
export type TDeleteCaDTO = { export type TDeleteCaDTO = {
@@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { caKeys } from "../ca/queries";
import { workspaceKeys } from "../workspace/queries"; import { workspaceKeys } from "../workspace/queries";
import { certTemplateKeys } from "./queries"; import { certTemplateKeys } from "./queries";
import { import {
@@ -23,8 +24,9 @@ export const useCreateCertTemplate = () => {
); );
return certificateTemplate; return certificateTemplate;
}, },
onSuccess: (_, { projectId }) => { onSuccess: ({ caId }, { projectId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId)); queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId));
queryClient.invalidateQueries(caKeys.getCaCertTemplates(caId));
} }
}); });
}; };
@@ -40,22 +42,25 @@ export const useUpdateCertTemplate = () => {
return certificateTemplate; return certificateTemplate;
}, },
onSuccess: (_, { projectId, id }) => { onSuccess: ({ caId }, { projectId, id }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId)); queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId));
queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id)); queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id));
queryClient.invalidateQueries(caKeys.getCaCertTemplates(caId));
} }
}); });
}; };
export const useDeleteCertTemplate = () => { export const useDeleteCertTemplate = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<void, {}, TDeleteCertificateTemplateDTO>({ return useMutation<TCertificateTemplate, {}, TDeleteCertificateTemplateDTO>({
mutationFn: async (data) => { mutationFn: async (data) => {
return apiRequest.delete(`/api/v1/pki/certificate-templates/${data.id}`); const { data: certificateTemplate } = await apiRequest.delete<TCertificateTemplate>(`/api/v1/pki/certificate-templates/${data.id}`);
return certificateTemplate;
}, },
onSuccess: (_, { projectId, id }) => { onSuccess: ({ caId }, { projectId, id }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId)); queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId));
queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id)); queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id));
queryClient.invalidateQueries(caKeys.getCaCertTemplates(caId));
} }
}); });
}; };
@@ -22,6 +22,7 @@ import { usePopUp } from "@app/hooks/usePopUp";
import { CaModal } from "@app/views/Project/CertificatesPage/components/CaTab/components/CaModal"; import { CaModal } from "@app/views/Project/CertificatesPage/components/CaTab/components/CaModal";
import { CaInstallCertModal } from "../CertificatesPage/components/CaTab/components/CaInstallCertModal"; import { CaInstallCertModal } from "../CertificatesPage/components/CaTab/components/CaInstallCertModal";
import { CertificateTemplatesSection } from "../CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection";
import { import {
CaCertificatesSection, CaCertificatesSection,
CaCrlsSection, CaCrlsSection,
@@ -125,6 +126,7 @@ export const CaPage = withProjectPermission(
</div> </div>
<div className="w-full"> <div className="w-full">
<CaCertificatesSection caId={caId} /> <CaCertificatesSection caId={caId} />
<CertificateTemplatesSection caId={caId} />
<CaCrlsSection caId={caId} /> <CaCrlsSection caId={caId} />
</div> </div>
</div> </div>
@@ -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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns"; import { format } from "date-fns";
@@ -33,6 +33,28 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="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">CA Details</h3> <h3 className="text-lg font-semibold text-mineshaft-100">CA Details</h3>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Identity}>
{(isAllowed) => {
return (
<Tooltip content="Edit Identity">
<IconButton
isDisabled={!isAllowed}
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("ca", {
caId: ca.id
});
}}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>
);
}}
</ProjectPermissionCan>
</div> </div>
<div className="pt-4"> <div className="pt-4">
<div className="mb-4"> <div className="mb-4">
@@ -115,6 +137,12 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
{ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"} {ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"}
</p> </p>
</div> </div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Template Issuance Required</p>
<p className="text-sm text-mineshaft-300">
{ca.requireTemplateForIssuance ? "True" : "False"}
</p>
</div>
{ca.status === CaStatus.ACTIVE && ( {ca.status === CaStatus.ACTIVE && (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
@@ -12,11 +12,12 @@ import {
Modal, Modal,
ModalContent, ModalContent,
Select, Select,
SelectItem SelectItem,
Switch
// DatePicker // DatePicker
} from "@app/components/v2"; } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { CaType, useCreateCa, useGetCaById } from "@app/hooks/api/ca"; import { CaType, useCreateCa, useGetCaById,useUpdateCa } from "@app/hooks/api/ca";
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants"; import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums"; import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -49,7 +50,8 @@ const schema = z
CertKeyAlgorithm.RSA_4096, CertKeyAlgorithm.RSA_4096,
CertKeyAlgorithm.ECDSA_P256, CertKeyAlgorithm.ECDSA_P256,
CertKeyAlgorithm.ECDSA_P384 CertKeyAlgorithm.ECDSA_P384
]) ]),
requireTemplateForIssuance: z.boolean()
}) })
.required(); .required();
@@ -70,7 +72,9 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
// const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false); // const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const { data: ca } = useGetCaById((popUp?.ca?.data as { caId: string })?.caId || ""); const { data: ca } = useGetCaById((popUp?.ca?.data as { caId: string })?.caId || "");
const { mutateAsync: createMutateAsync } = useCreateCa(); const { mutateAsync: createMutateAsync } = useCreateCa();
const { mutateAsync: updateMutateAsync } = useUpdateCa();
const { const {
control, control,
@@ -110,7 +114,8 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
commonName: ca.commonName, commonName: ca.commonName,
notAfter: ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "", notAfter: ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "",
maxPathLength: ca.maxPathLength ? String(ca.maxPathLength) : "", maxPathLength: ca.maxPathLength ? String(ca.maxPathLength) : "",
keyAlgorithm: ca.keyAlgorithm keyAlgorithm: ca.keyAlgorithm,
requireTemplateForIssuance: ca.requireTemplateForIssuance
}); });
} else { } else {
reset({ reset({
@@ -124,7 +129,8 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
commonName: "", commonName: "",
notAfter: getDateTenYearsFromToday(), notAfter: getDateTenYearsFromToday(),
maxPathLength: "-1", maxPathLength: "-1",
keyAlgorithm: CertKeyAlgorithm.RSA_2048 keyAlgorithm: CertKeyAlgorithm.RSA_2048,
requireTemplateForIssuance: true
}); });
} }
}, [ca]); }, [ca]);
@@ -140,31 +146,43 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
province, province,
notAfter, notAfter,
maxPathLength, maxPathLength,
keyAlgorithm keyAlgorithm,
requireTemplateForIssuance
}: FormData) => { }: FormData) => {
try { try {
if (!currentWorkspace?.slug) return; if (!currentWorkspace?.slug) return;
await createMutateAsync({ if (ca) {
projectSlug: currentWorkspace.slug, // update
type, await updateMutateAsync({
friendlyName, projectSlug: currentWorkspace.slug,
commonName, caId: ca.id,
organization, requireTemplateForIssuance
ou, });
country, } else {
province, // create
locality, await createMutateAsync({
notAfter, projectSlug: currentWorkspace.slug,
maxPathLength: Number(maxPathLength), type,
keyAlgorithm friendlyName,
}); commonName,
organization,
ou,
country,
province,
locality,
notAfter,
maxPathLength: Number(maxPathLength),
keyAlgorithm,
requireTemplateForIssuance
});
}
reset(); reset();
handlePopUpToggle("ca", false); handlePopUpToggle("ca", false);
createNotification({ createNotification({
text: "Successfully created CA", text: `Successfully ${ca ? "updated" : "created"} CA`,
type: "success" type: "success"
}); });
} catch (err) { } catch (err) {
@@ -406,7 +424,24 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
</FormControl> </FormControl>
)} )}
/> />
{!ca && ( <Controller
control={control}
name="requireTemplateForIssuance"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message} className="my-8">
<Switch
id="is-active"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Require Template for Certificate Issuance</p>
</Switch>
</FormControl>
);
}}
/>
{/* {!ca && ( */}
<div className="flex items-center"> <div className="flex items-center">
<Button <Button
className="mr-4" className="mr-4"
@@ -425,7 +460,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
Cancel Cancel
</Button> </Button>
</div> </div>
)} {/* )} */}
</form> </form>
</ModalContent> </ModalContent>
</Modal> </Modal>
@@ -3,7 +3,6 @@ import {
faBan, faBan,
faCertificate, faCertificate,
faEllipsis, faEllipsis,
faEye,
faTrash faTrash
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -155,28 +154,6 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
)} )}
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.CertificateAuthorities}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("ca", {
caId: ca.id
});
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEye} />}
>
View CA
</DropdownMenuItem>
)}
</ProjectPermissionCan>
{(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && ( {(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
@@ -1,7 +1,7 @@
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { PkiCollectionSection } from "../PkiAlertsTab/components"; import { PkiCollectionSection } from "../PkiAlertsTab/components";
import { CertificateTemplatesSection } from "./components/CertificateTemplatesSection"; // import { CertificateTemplatesSection } from "./components/CertificateTemplatesSection";
import { CertificatesSection } from "./components"; import { CertificatesSection } from "./components";
export const CertificatesTab = () => { export const CertificatesTab = () => {
@@ -14,7 +14,7 @@ export const CertificatesTab = () => {
exit={{ opacity: 0, translateX: 30 }} exit={{ opacity: 0, translateX: 30 }}
> >
<PkiCollectionSection /> <PkiCollectionSection />
<CertificateTemplatesSection /> {/* <CertificateTemplatesSection /> */}
<CertificatesSection /> <CertificatesSection />
</motion.div> </motion.div>
); );
@@ -21,11 +21,11 @@ import { useWorkspace } from "@app/context";
import { import {
CaStatus, CaStatus,
useCreateCertTemplate, useCreateCertTemplate,
useGetCaById,
useGetCertTemplate, useGetCertTemplate,
useListWorkspaceCas, useListWorkspaceCas,
useListWorkspacePkiCollections, useListWorkspacePkiCollections,
useUpdateCertTemplate useUpdateCertTemplate} from "@app/hooks/api";
} from "@app/hooks/api";
import { caTypeToNameMap } from "@app/hooks/api/ca/constants"; import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -51,6 +51,7 @@ const schema = z.object({
export type FormData = z.infer<typeof schema>; export type FormData = z.infer<typeof schema>;
type Props = { type Props = {
caId: string;
popUp: UsePopUpState<["certificateTemplate"]>; popUp: UsePopUpState<["certificateTemplate"]>;
handlePopUpToggle: ( handlePopUpToggle: (
popUpName: keyof UsePopUpState<["certificateTemplate"]>, popUpName: keyof UsePopUpState<["certificateTemplate"]>,
@@ -58,8 +59,11 @@ type Props = {
) => void; ) => void;
}; };
export const CertificateTemplateModal = ({ popUp, handlePopUpToggle }: Props) => { export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Props) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data: ca } = useGetCaById(caId);
const { data: certTemplate } = useGetCertTemplate( const { data: certTemplate } = useGetCertTemplate(
(popUp?.certificateTemplate?.data as { id: string })?.id || "" (popUp?.certificateTemplate?.data as { id: string })?.id || ""
); );
@@ -97,16 +101,15 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle }: Props) =>
}); });
} else { } else {
reset({ reset({
caId: "", caId,
name: "", name: "",
commonName: "", commonName: "",
ttl: "" ttl: ""
}); });
} }
}, [certTemplate]); }, [certTemplate, ca]);
const onFormSubmit = async ({ const onFormSubmit = async ({
caId,
collectionId, collectionId,
name, name,
commonName, commonName,
@@ -130,6 +133,8 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle }: Props) =>
ttl ttl
}); });
// TODO: requireTemplateForIssuance field
createNotification({ createNotification({
text: "Successfully updated certificate template", text: "Successfully updated certificate template",
type: "success" type: "success"
@@ -190,7 +195,7 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle }: Props) =>
<Controller <Controller
control={control} control={control}
name="caId" name="caId"
defaultValue="" defaultValue={caId}
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl <FormControl
label="Issuing CA" label="Issuing CA"
@@ -204,6 +209,7 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle }: Props) =>
{...field} {...field}
onValueChange={(e) => onChange(e)} onValueChange={(e) => onChange(e)}
className="w-full" className="w-full"
isDisabled
> >
{(cas || []).map(({ id, type, dn }) => ( {(cas || []).map(({ id, type, dn }) => (
<SelectItem value={id} key={`ca-${id}`}> <SelectItem value={id} key={`ca-${id}`}>
@@ -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 { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions"; 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 { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useDeleteCertTemplate } from "@app/hooks/api"; import { useDeleteCertTemplate } from "@app/hooks/api";
@@ -12,7 +16,11 @@ import { CertificateTemplateEnrollmentModal } from "./CertificateTemplateEnrollm
import { CertificateTemplateModal } from "./CertificateTemplateModal"; import { CertificateTemplateModal } from "./CertificateTemplateModal";
import { CertificateTemplatesTable } from "./CertificateTemplatesTable"; import { CertificateTemplatesTable } from "./CertificateTemplatesTable";
export const CertificateTemplatesSection = () => { type Props = {
caId: string;
}
export const CertificateTemplatesSection = ({ caId }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"certificateTemplate", "certificateTemplate",
"deleteCertificateTemplate", "deleteCertificateTemplate",
@@ -50,8 +58,8 @@ export const CertificateTemplatesSection = () => {
}; };
return ( return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between"> {/* <div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Certificate Templates</p> <p className="text-xl font-semibold text-mineshaft-100">Certificate Templates</p>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={ProjectPermissionActions.Create}
@@ -69,9 +77,30 @@ export const CertificateTemplatesSection = () => {
</Button> </Button>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
</div> */}
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Certificate Templates</h3>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.CertificateTemplates}
>
{(isAllowed) => (
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => handlePopUpOpen("certificateTemplate")}
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
)}
</ProjectPermissionCan>
</div> </div>
<CertificateTemplatesTable handlePopUpOpen={handlePopUpOpen} /> <div className="py-4">
<CertificateTemplateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <CertificateTemplatesTable handlePopUpOpen={handlePopUpOpen} caId={caId} />
</div>
<CertificateTemplateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} caId={caId} />
<CertificateTemplateEnrollmentModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <CertificateTemplateEnrollmentModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteCertificateTemplate.isOpen} isOpen={popUp.deleteCertificateTemplate.isOpen}
@@ -23,12 +23,15 @@ import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSub, ProjectPermissionSub,
useSubscription, useSubscription,
useWorkspace
} from "@app/context"; } from "@app/context";
import { useListWorkspaceCertificateTemplates } from "@app/hooks/api"; import {
// useListWorkspaceCertificateTemplates,
useGetCaCertTemplates
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = { type Props = {
caId: string;
handlePopUpOpen: ( handlePopUpOpen: (
popUpName: keyof UsePopUpState< popUpName: keyof UsePopUpState<
["certificateTemplate", "deleteCertificateTemplate", "enrollmentOptions", "upgradePlan"] ["certificateTemplate", "deleteCertificateTemplate", "enrollmentOptions", "upgradePlan"]
@@ -40,12 +43,14 @@ type Props = {
) => void; ) => void;
}; };
export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => { export const CertificateTemplatesTable = ({ handlePopUpOpen, caId }: Props) => {
const { currentWorkspace } = useWorkspace();
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { data, isLoading } = useListWorkspaceCertificateTemplates({
workspaceId: currentWorkspace?.id ?? "" const { data, isLoading } = useGetCaCertTemplates(caId);
});
// const { data, isLoading } = useListWorkspaceCertificateTemplates({
// workspaceId: currentWorkspace?.id ?? ""
// });
return ( return (
<div> <div>
@@ -54,7 +59,7 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => {
<THead> <THead>
<Tr> <Tr>
<Th>Name</Th> <Th>Name</Th>
<Th>Certificate Authority</Th> {/* <Th>Certificate Authority</Th> */}
<Th /> <Th />
</Tr> </Tr>
</THead> </THead>
@@ -65,13 +70,13 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => {
return ( return (
<Tr className="h-10" key={`certificate-${certificateTemplate.id}`}> <Tr className="h-10" key={`certificate-${certificateTemplate.id}`}>
<Td>{certificateTemplate.name}</Td> <Td>{certificateTemplate.name}</Td>
<Td>{certificateTemplate.caName}</Td> {/* <Td>{certificateTemplate.caName}</Td> */}
<Td className="flex justify-end"> <Td className="flex justify-end">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options"> <Tooltip content="More options">
<FontAwesomeIcon size="lg" icon={faEllipsis} /> <FontAwesomeIcon size="sm" icon={faEllipsis} />
</Tooltip> </Tooltip>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -143,7 +148,7 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => {
</TBody> </TBody>
</Table> </Table>
{!isLoading && !data?.certificateTemplates?.length && ( {!isLoading && !data?.certificateTemplates?.length && (
<EmptyState title="No certificate templates have been created" icon={faFileAlt} /> <EmptyState title="No certificate templates have been created for this CA" icon={faFileAlt} />
)} )}
</TableContainer> </TableContainer>
</div> </div>