Merge pull request #2397 from Infisical/cert-template-enforcement
Certificate Template Enforcement Option + PKI UX Improvements
@@ -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 hasRequireTemplateForIssuanceColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.CertificateAuthority,
|
||||||
|
"requireTemplateForIssuance"
|
||||||
|
);
|
||||||
|
if (!hasRequireTemplateForIssuanceColumn) {
|
||||||
|
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>;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(false)
|
||||||
|
.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",
|
||||||
|
|||||||
@@ -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,111 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Certificate Templates"
|
|
||||||
sidebarTitle: "Certificate Templates"
|
|
||||||
description: "Learn how to use certificate templates to enforce policies."
|
|
||||||
---
|
|
||||||
|
|
||||||
## Concept
|
|
||||||
|
|
||||||
In order to ensure your certificates follow certain policies, you can use certificate templates during the issuance and signing flows.
|
|
||||||
|
|
||||||
A certificate template is linked to a certificate authority. It contains custom policies for certificate fields, allowing you to define rules based on your security policies.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
The typical workflow for using certificate templates consists of the following steps:
|
|
||||||
|
|
||||||
1. Creating a certificate template attached to an existing CA along with defining custom rules for certificate fields.
|
|
||||||
2. Selecting the certificate template during the creation of new certificates.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
Note that this workflow can be executed via the Infisical UI or manually such
|
|
||||||
as via API.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
## Guide to using Certificate Templates
|
|
||||||
|
|
||||||
In the following steps, we explore how to issue a X.509 certificate using a certificate template.
|
|
||||||
|
|
||||||
<Tabs>
|
|
||||||
<Tab title="Infisical UI">
|
|
||||||
|
|
||||||
<Steps>
|
|
||||||
<Step title="Creating the certificate template">
|
|
||||||
To create a certificate template, head to your Project > Internal PKI > Certificate Templates and press **Create Certificate Template**.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Here, set the **Issuing CA** to the CA you want to issue certificates under when the certificate template is used.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Here's some guidance on each field:
|
|
||||||
- Template Name: A descriptive name for the certificate template.
|
|
||||||
- Issuing CA: The Certificate Authority (CA) that will issue certificates based on this template.
|
|
||||||
- Certificate Collection: The collection where certificates issued with this template will be added.
|
|
||||||
- Common Name (CN): The regular expression used to validate the common name in certificate requests.
|
|
||||||
- Alternative Names (SANs): The regular expression used to validate subject alternative names in certificate requests.
|
|
||||||
- TTL: The maximum Time-to-Live (TTL) for certificates issued using this template.
|
|
||||||
|
|
||||||
</Step>
|
|
||||||
<Step title="Using the certificate template">
|
|
||||||
Once you have created the certificate template from step 1, you can select it when issuing certificates.
|
|
||||||
|
|
||||||

|
|
||||||
</Step>
|
|
||||||
</Steps>
|
|
||||||
</Tab>
|
|
||||||
<Tab title="API">
|
|
||||||
<Steps>
|
|
||||||
<Step title="Creating the certificate template">
|
|
||||||
To create a certificate template, make an API request to the [Create Certificate Template](/api-reference/endpoints/certificate-templates/create) API endpoint.
|
|
||||||
|
|
||||||
### Sample request
|
|
||||||
|
|
||||||
```bash Request
|
|
||||||
curl --request POST \
|
|
||||||
--url https://app.infisical.com/api/v1/pki/certificate-templates \
|
|
||||||
--header 'Content-Type: application/json' \
|
|
||||||
--data '{
|
|
||||||
"caId": "<string>",
|
|
||||||
"pkiCollectionId": "<string>",
|
|
||||||
"name": "<string>",
|
|
||||||
"commonName": "<string>",
|
|
||||||
"subjectAlternativeName": "<string>",
|
|
||||||
"ttl": "<string>"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sample response
|
|
||||||
|
|
||||||
```bash Response
|
|
||||||
{
|
|
||||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
|
||||||
"caId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
|
||||||
"name": "certificate-template-1",
|
|
||||||
"commonName": "<string>",
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
<Step title="Using the certificate template">
|
|
||||||
To use the certificate template, attach the certificate template ID when invoking the API endpoint for [issuing](/api-reference/endpoints/certificates/issue-certificate) or [signing](/api-reference/endpoints/certificates/sign-certificate) new certificates.
|
|
||||||
|
|
||||||
### Sample request
|
|
||||||
|
|
||||||
```bash Request
|
|
||||||
curl --request POST \
|
|
||||||
--url https://app.infisical.com/api/v1/pki/certificates/issue-certificate \
|
|
||||||
--header 'Content-Type: application/json' \
|
|
||||||
--data '{
|
|
||||||
"certificateTemplateId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
|
||||||
"friendlyName": "my-new-certificate",
|
|
||||||
"commonName": "CERT",
|
|
||||||
...
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
</Steps>
|
|
||||||
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
@@ -25,7 +25,7 @@ graph TD
|
|||||||
|
|
||||||
The typical workflow for managing certificates consists of the following steps:
|
The typical workflow for managing certificates consists of the following steps:
|
||||||
|
|
||||||
1. Issuing a certificate under an intermediate CA with details like name and validity period.
|
1. Issuing a certificate under an intermediate CA with details like name and validity period. As part of certificate issuance, you can either issue a certificate directly from a CA or do it via a certificate template.
|
||||||
2. Managing certificate lifecycle events such as certificate renewal and revocation. As part of the certificate revocation flow,
|
2. Managing certificate lifecycle events such as certificate renewal and revocation. As part of the certificate revocation flow,
|
||||||
you can also query for a Certificate Revocation List [CRL](https://en.wikipedia.org/wiki/Certificate_revocation_list), a time-stamped, signed
|
you can also query for a Certificate Revocation List [CRL](https://en.wikipedia.org/wiki/Certificate_revocation_list), a time-stamped, signed
|
||||||
data structure issued by a CA containing a list of revoked certificates to check if a certificate has been revoked.
|
data structure issued by a CA containing a list of revoked certificates to check if a certificate has been revoked.
|
||||||
@@ -43,28 +43,51 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
|||||||
<Tab title="Infisical UI">
|
<Tab title="Infisical UI">
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
|
<Step title="Creating a certificate template">
|
||||||
|
A certificate template is a set of policies for certificates issued under that template; each template is bound to a specific CA and can also be bound to a certificate collection for alerting such that any certificate issued under the template is automatically added to the collection.
|
||||||
|
|
||||||
|
With certificate templates, you can specify, for example, that issued certificates must have a common name (CN) adhering to a specific format like `.*.acme.com` or perhaps that the max TTL cannot be more than 1 year.
|
||||||
|
|
||||||
|
Head to your Project > Certificate Authorities > Your Issuing CA and create a certificate template.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here's some guidance on each field:
|
||||||
|
|
||||||
|
- Template Name: A name for the certificate template.
|
||||||
|
- Issuing CA: The Certificate Authority (CA) that will issue certificates based on this template.
|
||||||
|
- Certificate Collection (Optional): The certificate collection that certificates should be added to when issued under the template.
|
||||||
|
- Common Name (CN): A regular expression used to validate the common name in certificate requests.
|
||||||
|
- Alternative Names (SANs): A regular expression used to validate subject alternative names in certificate requests.
|
||||||
|
- TTL: The maximum Time-to-Live (TTL) for certificates issued using this template.
|
||||||
|
</Step>
|
||||||
<Step title="Creating a certificate">
|
<Step title="Creating a certificate">
|
||||||
To create a certificate, head to your Project > Internal PKI > Certificates and press **Create Certificate**.
|
To create a certificate, head to your Project > Internal PKI > Certificates and press **Issue** under the Certificates section.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Here, set the **CA** to the CA you want to issue the certificate under and fill out details for the certificate.
|
Here, set the **Certificate Template** to the template from step 1 and fill out the rest of the details for the certificate to be issued.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Here's some guidance on each field:
|
Here's some guidance on each field:
|
||||||
|
|
||||||
- Issuing CA: The CA under which to issue the certificate.
|
|
||||||
- Friendly Name: A friendly name for the certificate; this is only for display and defaults to the common name of the certificate if left empty.
|
- Friendly Name: A friendly name for the certificate; this is only for display and defaults to the common name of the certificate if left empty.
|
||||||
- Common Name (CN): The (common) name for the certificate like `service.acme.com`.
|
- Common Name (CN): The (common) name for the certificate like `service.acme.com`.
|
||||||
- Alternative Names (SANs): A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses like `app1.acme.com, app2.acme.com`.
|
- Alternative Names (SANs): A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses like `app1.acme.com, app2.acme.com`.
|
||||||
- TTL: The lifetime of the certificate in seconds.
|
- TTL: The lifetime of the certificate in seconds.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Note that Infisical PKI supports issuing certificates without certificate templates as well. If this is desired, then you can set the **Certificate Template** field to **None**
|
||||||
|
and specify the **Issuing CA** and optional **Certificate Collection** fields; the rest of the fields for the issued certificate remain the same.
|
||||||
|
|
||||||
|
That said, we recommend using certificate templates to enforce policies and attach expiration monitoring on issued certificates.
|
||||||
|
</Note>
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Copying the certificate details">
|
<Step title="Copying the certificate details">
|
||||||
Once you have created the certificate from step 1, you'll be presented with the certificate details including the **Certificate Body**, **Certificate Chain**, and **Private Key**.
|
Once you have created the certificate from step 1, you'll be presented with the certificate details including the **Certificate Body**, **Certificate Chain**, and **Private Key**.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
Make sure to download and store the **Private Key** in a secure location as it will only be displayed once at the time of certificate issuance.
|
Make sure to download and store the **Private Key** in a secure location as it will only be displayed once at the time of certificate issuance.
|
||||||
@@ -74,16 +97,54 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
|||||||
</Steps>
|
</Steps>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="API">
|
<Tab title="API">
|
||||||
To create a certificate, make an API request to the [Issue Certificate](/api-reference/endpoints/certificates/issue-cert) API endpoint,
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Creating a certificate template">
|
||||||
|
A certificate template is a set of policies for certificates issued under that template; each template is bound to a specific CA and can also be bound to a certificate collection for alerting such that any certificate issued under the template is automatically added to the collection.
|
||||||
|
|
||||||
|
With certificate templates, you can specify, for example, that issued certificates must have a common name (CN) adhering to a specific format like .*.acme.com or perhaps that the max TTL cannot be more than 1 year.
|
||||||
|
|
||||||
|
To create a certificate template, make an API request to the [Create Certificate Template](/api-reference/endpoints/certificate-templates/create) API endpoint, specifying the issuing CA.
|
||||||
|
|
||||||
|
### Sample request
|
||||||
|
|
||||||
|
```bash Request
|
||||||
|
curl --location --request POST 'https://app.infisical.com/api/v1/pki/certificate-templates' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data-raw '{
|
||||||
|
"caId": "<ca-id>",
|
||||||
|
"name": "My Certificate Template",
|
||||||
|
"commonName": ".*.acme.com",
|
||||||
|
"subjectAlternativeName": ".*.acme.com",
|
||||||
|
"ttl": "1y",
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample response
|
||||||
|
|
||||||
|
```bash Response
|
||||||
|
{
|
||||||
|
id: "...",
|
||||||
|
caId: "...",
|
||||||
|
name: "...",
|
||||||
|
commonName: "...",
|
||||||
|
subjectAlternativeName: "...",
|
||||||
|
ttl: "...",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
<Step title="Creating a certificate">
|
||||||
|
To create a certificate under the certificate template, make an API request to the [Issue Certificate](/api-reference/endpoints/certificates/issue-cert) API endpoint,
|
||||||
specifying the issuing CA.
|
specifying the issuing CA.
|
||||||
|
|
||||||
### Sample request
|
### Sample request
|
||||||
|
|
||||||
```bash Request
|
```bash Request
|
||||||
curl --location --request POST 'https://app.infisical.com/api/v1/pki/ca/<ca-id>/issue-certificate' \
|
curl --location --request POST 'https://app.infisical.com/api/v1/pki/certificates/issue-certificate' \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"commonName": "My Certificate",
|
"certificateTemplateId": "<certificate-template-id>",
|
||||||
|
"commonName": "service.acme.com",
|
||||||
"ttl": "1y",
|
"ttl": "1y",
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
@@ -100,18 +161,26 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Note that Infisical PKI supports issuing certificates without certificate templates as well. If this is desired, then you can set the **Certificate Template** field to **None**
|
||||||
|
and specify the **Issuing CA** and optional **Certificate Collection** fields; the rest of the fields for the issued certificate remain the same.
|
||||||
|
|
||||||
|
That said, we recommend using certificate templates to enforce policies and attach expiration monitoring on issued certificates.
|
||||||
|
</Note>
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
Make sure to store the `privateKey` as it is only returned once here at the time of certificate issuance. The `certificate` and `certificateChain` will remain accessible and can be retrieved at any time.
|
Make sure to store the `privateKey` as it is only returned once here at the time of certificate issuance. The `certificate` and `certificateChain` will remain accessible and can be retrieved at any time.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
If you have an external private key, you can also create a certificate by making an API request containing a pem-encoded CSR (Certificate Signing Request) to the [Sign Certificate](/api-reference/endpoints/certificates/sign-cert) API endpoint, specifying the issuing CA.
|
If you have an external private key, you can also create a certificate by making an API request containing a pem-encoded CSR (Certificate Signing Request) to the [Sign Certificate](/api-reference/endpoints/certificates/sign-certificate) API endpoint, specifying the issuing CA.
|
||||||
|
|
||||||
### Sample request
|
### Sample request
|
||||||
|
|
||||||
```bash Request
|
```bash Request
|
||||||
curl --location --request POST 'https://app.infisical.com/api/v1/pki/ca/<ca-id>/sign-certificate' \
|
curl --location --request POST 'https://app.infisical.com/api/v1/pki/certificates/sign-certificate' \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
|
"certificateTemplateId": "<certificate-template-id>",
|
||||||
"csr": "...",
|
"csr": "...",
|
||||||
"ttl": "1y",
|
"ttl": "1y",
|
||||||
}'
|
}'
|
||||||
@@ -128,7 +197,8 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
|||||||
serialNumber: "..."
|
serialNumber: "..."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ These endpoints are exposed on port 8443 under the .well-known/est path e.g.
|
|||||||
|
|
||||||
## Guide to configuring EST
|
## Guide to configuring EST
|
||||||
|
|
||||||
1. Set up a certificate template with your selected issuing CA. This template will define the policies and parameters for certificates issued through EST. For detailed instructions on configuring a certificate template, refer to the certificate templates [documentation](/documentation/platform/pki/certificate-templates).
|
1. Set up a certificate template with your selected issuing CA. This template will define the policies and parameters for certificates issued through EST. For detailed instructions on configuring a certificate template, refer to the certificate templates [documentation](/documentation/platform/pki/certificates#guide-to-issuing-certificates).
|
||||||
|
|
||||||
2. Proceed to the certificate template's enrollment settings
|
2. Proceed to the certificate template's enrollment settings
|
||||||

|

|
||||||
|
|||||||
@@ -214,10 +214,13 @@ In the following steps, we explore how to install the Infisical PKI Issuer using
|
|||||||
|
|
||||||
Data
|
Data
|
||||||
====
|
====
|
||||||
|
ca.crt: 1306 bytes
|
||||||
|
tls.crt: 2380 bytes
|
||||||
tls.key: 227 bytes
|
tls.key: 227 bytes
|
||||||
tls.crt: 912 bytes
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Here, `ca.crt` is the Root CA certificate, `tls.crt` is the requested certificate followed by the certificate chain, and `tls.key` is the private key for the certificate.
|
||||||
|
|
||||||
We can decode the certificate and print it out using `openssl`:
|
We can decode the certificate and print it out using `openssl`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ consisting of an (optional) root CA and an intermediate CA.
|
|||||||
- State or Province Name: The state or province.
|
- State or Province Name: The state or province.
|
||||||
- Locality Name: The city or locality.
|
- Locality Name: The city or locality.
|
||||||
- Common Name: The name of the CA.
|
- Common Name: The name of the CA.
|
||||||
|
- Require Template for Certificate Issuance: Whether or not certificates for this CA can only be issued through certificate templates (recommended).
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
The Organization, Country, State or Province Name, Locality Name, and Common Name make up the **Distinguished Name (DN)** or **subject** of the CA.
|
The Organization, Country, State or Province Name, Locality Name, and Common Name make up the **Distinguished Name (DN)** or **subject** of the CA.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 721 KiB |
|
Before Width: | Height: | Size: 372 KiB |
|
Before Width: | Height: | Size: 579 KiB |
|
Before Width: | Height: | Size: 715 KiB |
|
Before Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 779 KiB |
|
After Width: | Height: | Size: 379 KiB |
|
After Width: | Height: | Size: 700 KiB |
|
After Width: | Height: | Size: 517 KiB |
|
Before Width: | Height: | Size: 462 KiB |
|
Before Width: | Height: | Size: 723 KiB After Width: | Height: | Size: 865 KiB |
@@ -108,7 +108,6 @@
|
|||||||
"documentation/platform/pki/overview",
|
"documentation/platform/pki/overview",
|
||||||
"documentation/platform/pki/private-ca",
|
"documentation/platform/pki/private-ca",
|
||||||
"documentation/platform/pki/certificates",
|
"documentation/platform/pki/certificates",
|
||||||
"documentation/platform/pki/certificate-templates",
|
|
||||||
"documentation/platform/pki/pki-issuer",
|
"documentation/platform/pki/pki-issuer",
|
||||||
"documentation/platform/pki/est",
|
"documentation/platform/pki/est",
|
||||||
"documentation/platform/pki/alerting"
|
"documentation/platform/pki/alerting"
|
||||||
@@ -708,7 +707,7 @@
|
|||||||
"api-reference/endpoints/certificate-authorities/import-cert",
|
"api-reference/endpoints/certificate-authorities/import-cert",
|
||||||
"api-reference/endpoints/certificate-authorities/issue-cert",
|
"api-reference/endpoints/certificate-authorities/issue-cert",
|
||||||
"api-reference/endpoints/certificate-authorities/sign-cert",
|
"api-reference/endpoints/certificate-authorities/sign-cert",
|
||||||
"api-reference/endpoints/certificate-authorities/crls"
|
"api-reference/endpoints/certificate-authorities/crl"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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 CA">
|
||||||
|
<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) {
|
||||||
@@ -186,6 +204,11 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
>
|
>
|
||||||
<ModalContent title={`${ca ? "View" : "Create"} Private CA`}>
|
<ModalContent title={`${ca ? "View" : "Create"} Private CA`}>
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
{ca && (
|
||||||
|
<FormControl label="CA ID">
|
||||||
|
<Input value={ca.id} isDisabled className="bg-white/[0.07]" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="type"
|
name="type"
|
||||||
@@ -406,26 +429,41 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{!ca && (
|
<Controller
|
||||||
<div className="flex items-center">
|
control={control}
|
||||||
<Button
|
name="requireTemplateForIssuance"
|
||||||
className="mr-4"
|
render={({ field, fieldState: { error } }) => {
|
||||||
size="sm"
|
return (
|
||||||
type="submit"
|
<FormControl isError={Boolean(error)} errorText={error?.message} className="my-8">
|
||||||
isLoading={isSubmitting}
|
<Switch
|
||||||
isDisabled={isSubmitting}
|
id="is-active"
|
||||||
>
|
onCheckedChange={(value) => field.onChange(value)}
|
||||||
{popUp?.ca?.data ? "Update" : "Create"}
|
isChecked={field.value}
|
||||||
</Button>
|
>
|
||||||
<Button
|
<p className="w-full">Require Template for Certificate Issuance</p>
|
||||||
colorSchema="secondary"
|
</Switch>
|
||||||
variant="plain"
|
</FormControl>
|
||||||
onClick={() => handlePopUpToggle("ca", false)}
|
);
|
||||||
>
|
}}
|
||||||
Cancel
|
/>
|
||||||
</Button>
|
<div className="flex items-center">
|
||||||
</div>
|
<Button
|
||||||
)}
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{popUp?.ca?.data ? "Update" : "Create"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("ca", false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -218,7 +218,6 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
}
|
}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
className="mt-4"
|
|
||||||
isRequired
|
isRequired
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -172,6 +175,11 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle }: Props) =>
|
|||||||
>
|
>
|
||||||
<ModalContent title={certTemplate ? "Certificate Template" : "Create Certificate Template"}>
|
<ModalContent title={certTemplate ? "Certificate Template" : "Create Certificate Template"}>
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
{certTemplate && (
|
||||||
|
<FormControl label="Certificate Template ID">
|
||||||
|
<Input value={certTemplate.id} isDisabled className="bg-white/[0.07]" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
@@ -190,7 +198,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 +212,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,28 +58,30 @@ 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="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Certificate Templates</p>
|
<h3 className="text-lg font-semibold text-mineshaft-100">Certificate Templates</h3>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionActions.Create}
|
||||||
a={ProjectPermissionSub.CertificateTemplates}
|
a={ProjectPermissionSub.CertificateTemplates}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<IconButton
|
||||||
colorSchema="primary"
|
ariaLabel="copy icon"
|
||||||
type="submit"
|
variant="plain"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
className="group relative"
|
||||||
onClick={() => handlePopUpOpen("certificateTemplate")}
|
onClick={() => handlePopUpOpen("certificateTemplate")}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Create
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
</Button>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</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,10 @@ 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);
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -54,7 +55,6 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Name</Th>
|
<Th>Name</Th>
|
||||||
<Th>Certificate Authority</Th>
|
|
||||||
<Th />
|
<Th />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
@@ -65,13 +65,12 @@ 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 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 +142,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>
|
||||||
|
|||||||