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

View File

@@ -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");
});
}
}

View File

@@ -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<typeof CertificateAuthoritiesSchema>;

View File

@@ -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<typeof SecretSharingSchema>;

View File

@@ -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

View File

@@ -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"

View File

@@ -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",

View File

@@ -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"]

View File

@@ -41,6 +41,7 @@ import {
TCreateCaDTO,
TDeleteCaDTO,
TGetCaCertDTO,
TGetCaCertificateTemplatesDTO,
TGetCaCertsDTO,
TGetCaCsrDTO,
TGetCaDTO,
@@ -64,7 +65,7 @@ type TCertificateAuthorityServiceFactoryDep = {
>;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">;
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById">;
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById" | "find">;
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
@@ -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
};
};

View File

@@ -38,6 +38,7 @@ export type TCreateCaDTO = {
notAfter?: string;
maxPathLength: number;
keyAlgorithm: CertKeyAlgorithm;
requireTemplateForIssuance: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TGetCaDTO = {
@@ -47,6 +48,7 @@ export type TGetCaDTO = {
export type TUpdateCaDTO = {
caId: string;
status?: CaStatus;
requireTemplateForIssuance?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TDeleteCaDTO = {
@@ -125,6 +127,10 @@ export type TSignCertFromCaDTO =
notAfter?: string;
} & Omit<TProjectPermission, "projectId">);
export type TGetCaCertificateTemplatesDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
export type TDNParts = {
commonName?: string;
organization?: string;

View File

@@ -4,7 +4,6 @@
"requires": true,
"packages": {
"": {
"name": "frontend",
"dependencies": {
"@casl/ability": "^6.5.0",
"@casl/react": "^3.1.0",

View File

@@ -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";

View File

@@ -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));
}
});
};

View File

@@ -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)
});
};

View File

@@ -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 = {

View File

@@ -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<void, {}, TDeleteCertificateTemplateDTO>({
return useMutation<TCertificateTemplate, {}, TDeleteCertificateTemplateDTO>({
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(certTemplateKeys.getCertTemplateById(id));
queryClient.invalidateQueries(caKeys.getCaCertTemplates(caId));
}
});
};

View File

@@ -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(
</div>
<div className="w-full">
<CaCertificatesSection caId={caId} />
<CertificateTemplatesSection caId={caId} />
<CaCrlsSection caId={caId} />
</div>
</div>

View File

@@ -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) => {
<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">
<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 className="pt-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") : "-"}
</p>
</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 && (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}

View File

@@ -12,11 +12,12 @@ import {
Modal,
ModalContent,
Select,
SelectItem
SelectItem,
Switch
// DatePicker
} from "@app/components/v2";
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 { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -49,7 +50,8 @@ const schema = z
CertKeyAlgorithm.RSA_4096,
CertKeyAlgorithm.ECDSA_P256,
CertKeyAlgorithm.ECDSA_P384
])
]),
requireTemplateForIssuance: z.boolean()
})
.required();
@@ -70,7 +72,9 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
// 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) => {
</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">
<Button
className="mr-4"
@@ -425,7 +460,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
Cancel
</Button>
</div>
)}
{/* )} */}
</form>
</ModalContent>
</Modal>

View File

@@ -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) => {
)}
</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) && (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}

View File

@@ -1,7 +1,7 @@
import { motion } from "framer-motion";
import { PkiCollectionSection } from "../PkiAlertsTab/components";
import { CertificateTemplatesSection } from "./components/CertificateTemplatesSection";
// import { CertificateTemplatesSection } from "./components/CertificateTemplatesSection";
import { CertificatesSection } from "./components";
export const CertificatesTab = () => {
@@ -14,7 +14,7 @@ export const CertificatesTab = () => {
exit={{ opacity: 0, translateX: 30 }}
>
<PkiCollectionSection />
<CertificateTemplatesSection />
{/* <CertificateTemplatesSection /> */}
<CertificatesSection />
</motion.div>
);

View File

@@ -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<typeof schema>;
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) =>
<Controller
control={control}
name="caId"
defaultValue=""
defaultValue={caId}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Issuing CA"
@@ -204,6 +209,7 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle }: Props) =>
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
isDisabled
>
{(cas || []).map(({ id, type, dn }) => (
<SelectItem value={id} key={`ca-${id}`}>

View File

@@ -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 (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
{/* <div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Certificate Templates</p>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
@@ -69,9 +77,30 @@ export const CertificateTemplatesSection = () => {
</Button>
)}
</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>
<CertificateTemplatesTable handlePopUpOpen={handlePopUpOpen} />
<CertificateTemplateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<div className="py-4">
<CertificateTemplatesTable handlePopUpOpen={handlePopUpOpen} caId={caId} />
</div>
<CertificateTemplateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} caId={caId} />
<CertificateTemplateEnrollmentModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteCertificateTemplate.isOpen}

View File

@@ -23,12 +23,15 @@ import {
ProjectPermissionActions,
ProjectPermissionSub,
useSubscription,
useWorkspace
} from "@app/context";
import { useListWorkspaceCertificateTemplates } from "@app/hooks/api";
import {
// useListWorkspaceCertificateTemplates,
useGetCaCertTemplates
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
caId: string;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
["certificateTemplate", "deleteCertificateTemplate", "enrollmentOptions", "upgradePlan"]
@@ -40,12 +43,14 @@ type Props = {
) => 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 (
<div>
@@ -54,7 +59,7 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => {
<THead>
<Tr>
<Th>Name</Th>
<Th>Certificate Authority</Th>
{/* <Th>Certificate Authority</Th> */}
<Th />
</Tr>
</THead>
@@ -65,13 +70,13 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => {
return (
<Tr className="h-10" key={`certificate-${certificateTemplate.id}`}>
<Td>{certificateTemplate.name}</Td>
<Td>{certificateTemplate.caName}</Td>
{/* <Td>{certificateTemplate.caName}</Td> */}
<Td className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="lg" icon={faEllipsis} />
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
@@ -143,7 +148,7 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => {
</TBody>
</Table>
{!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>
</div>