Merge pull request #2397 from Infisical/cert-template-enforcement

Certificate Template Enforcement Option + PKI UX Improvements
This commit is contained in:
BlackMagiq
2024-09-10 09:19:37 -07:00
committed by GitHub
41 changed files with 448 additions and 250 deletions
@@ -0,0 +1,25 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
const 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>;
+2 -2
View File
@@ -21,8 +21,8 @@ export const SecretSharingSchema = z.object({
expiresAfterViews: z.number().nullable().optional(), expiresAfterViews: z.number().nullable().optional(),
accessType: z.string().default("anyone"), accessType: z.string().default("anyone"),
name: z.string().nullable().optional(), name: z.string().nullable().optional(),
password: z.string().nullable().optional(), lastViewedAt: z.date().nullable().optional(),
lastViewedAt: z.date().nullable().optional() password: z.string().nullable().optional()
}); });
export type TSecretSharing = z.infer<typeof SecretSharingSchema>; export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
@@ -140,6 +140,7 @@ export enum EventType {
GET_CA_CRLS = "get-certificate-authority-crls", GET_CA_CRLS = "get-certificate-authority-crls",
ISSUE_CERT = "issue-cert", ISSUE_CERT = "issue-cert",
SIGN_CERT = "sign-cert", SIGN_CERT = "sign-cert",
GET_CA_CERTIFICATE_TEMPLATES = "get-ca-certificate-templates",
GET_CERT = "get-cert", GET_CERT = "get-cert",
DELETE_CERT = "delete-cert", DELETE_CERT = "delete-cert",
REVOKE_CERT = "revoke-cert", REVOKE_CERT = "revoke-cert",
@@ -1192,6 +1193,14 @@ interface SignCert {
}; };
} }
interface GetCaCertificateTemplates {
type: EventType.GET_CA_CERTIFICATE_TEMPLATES;
metadata: {
caId: string;
dn: string;
};
}
interface GetCert { interface GetCert {
type: EventType.GET_CERT; type: EventType.GET_CERT;
metadata: { metadata: {
@@ -1547,6 +1556,7 @@ export type Event =
| GetCaCrls | GetCaCrls
| IssueCert | IssueCert
| SignCert | SignCert
| GetCaCertificateTemplates
| GetCert | GetCert
| DeleteCert | DeleteCert
| RevokeCert | RevokeCert
+6 -2
View File
@@ -1037,14 +1037,18 @@ export const CERTIFICATE_AUTHORITIES = {
maxPathLength: maxPathLength:
"The maximum number of intermediate CAs that may follow this CA in the certificate / CA chain. A maxPathLength of -1 implies no path limit on the chain.", "The maximum number of intermediate CAs that may follow this CA in the certificate / CA chain. A maxPathLength of -1 implies no path limit on the chain.",
keyAlgorithm: keyAlgorithm:
"The type of public key algorithm and size, in bits, of the key pair for the CA; when you create an intermediate CA, you must use a key algorithm supported by the parent CA." "The type of public key algorithm and size, in bits, of the key pair for the CA; when you create an intermediate CA, you must use a key algorithm supported by the parent CA.",
requireTemplateForIssuance:
"Whether or not certificates for this CA can only be issued through certificate templates."
}, },
GET: { GET: {
caId: "The ID of the CA to get" caId: "The ID of the CA to get"
}, },
UPDATE: { UPDATE: {
caId: "The ID of the CA to update", caId: "The ID of the CA to update",
status: "The status of the CA to update to. This can be one of active or disabled" status: "The status of the CA to update to. This can be one of active or disabled",
requireTemplateForIssuance:
"Whether or not certificates for this CA can only be issued through certificate templates."
}, },
DELETE: { DELETE: {
caId: "The ID of the CA to delete" caId: "The ID of the CA to delete"
@@ -1,7 +1,7 @@
import ms from "ms"; import ms from "ms";
import { z } from "zod"; import { z } from "zod";
import { CertificateAuthoritiesSchema } from "@app/db/schemas"; import { CertificateAuthoritiesSchema, CertificateTemplatesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs"; import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -42,7 +42,11 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
keyAlgorithm: z keyAlgorithm: z
.nativeEnum(CertKeyAlgorithm) .nativeEnum(CertKeyAlgorithm)
.default(CertKeyAlgorithm.RSA_2048) .default(CertKeyAlgorithm.RSA_2048)
.describe(CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm) .describe(CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm),
requireTemplateForIssuance: z
.boolean()
.default(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**.
![certificate-template create template dashboard](/images/platform/pki/certificate-template/create-template-dashboard.png)
Here, set the **Issuing CA** to the CA you want to issue certificates under when the certificate template is used.
![certificate-template create template modal](/images/platform/pki/certificate-template/create-template-form.png)
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.
![certificate-template select template](/images/platform/pki/certificate-template/select-template.png)
</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.
![pki certificate template modal](/images/platform/pki/certificate/cert-template-modal.png)
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.
![pki issue certificate](/images/platform/pki/cert-issue.png) ![pki issue certificate](/images/platform/pki/certificate/cert-issue.png)
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.
![pki issue certificate modal](/images/platform/pki/cert-issue-modal.png) ![pki issue certificate modal](/images/platform/pki/certificate/cert-issue-modal.png)
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**.
![pki certificate body](/images/platform/pki/cert-body.png) ![pki certificate body](/images/platform/pki/certificate/cert-body.png)
<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>
+1 -1
View File
@@ -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
![est enrollment dashboard](/images/platform/pki/est/template-enroll-hover.png) ![est enrollment dashboard](/images/platform/pki/est/template-enroll-hover.png)
@@ -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.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 579 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 715 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 723 KiB

After

Width:  |  Height:  |  Size: 865 KiB

+1 -2
View File
@@ -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"
] ]
}, },
{ {
-1
View File
@@ -4,7 +4,6 @@
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "frontend",
"dependencies": { "dependencies": {
"@casl/ability": "^6.5.0", "@casl/ability": "^6.5.0",
"@casl/react": "^3.1.0", "@casl/react": "^3.1.0",
+1 -1
View File
@@ -8,4 +8,4 @@ export {
useSignIntermediate, useSignIntermediate,
useUpdateCa useUpdateCa
} from "./mutations"; } from "./mutations";
export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrls, useGetCaCsr } from "./queries"; export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCertTemplates,useGetCaCrls, useGetCaCsr } from "./queries";
+2 -1
View File
@@ -43,8 +43,9 @@ export const useUpdateCa = () => {
} = await apiRequest.patch<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`, body); } = await apiRequest.patch<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`, body);
return ca; return ca;
}, },
onSuccess: (_, { projectSlug }) => { onSuccess: ({ id }, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug })); queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
queryClient.invalidateQueries(caKeys.getCaById(id));
} }
}); });
}; };
+15
View File
@@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { TCertificateTemplate } from "../certificateTemplates/types";
import { TCertificateAuthority } from "./types"; import { TCertificateAuthority } from "./types";
export const caKeys = { export const caKeys = {
@@ -11,6 +12,7 @@ export const caKeys = {
getCaCert: (caId: string) => [{ caId }, "ca-cert"], getCaCert: (caId: string) => [{ caId }, "ca-cert"],
getCaCsr: (caId: string) => [{ caId }, "ca-csr"], getCaCsr: (caId: string) => [{ caId }, "ca-csr"],
getCaCrl: (caId: string) => [{ caId }, "ca-crl"], getCaCrl: (caId: string) => [{ caId }, "ca-crl"],
getCaCertTemplates: (caId: string) => [{ caId }, "ca-cert-templates"],
getCaEstConfig: (caId: string) => [{ caId }, "ca-est-config"] getCaEstConfig: (caId: string) => [{ caId }, "ca-est-config"]
}; };
@@ -90,3 +92,16 @@ export const useGetCaCrls = (caId: string) => {
enabled: Boolean(caId) enabled: Boolean(caId)
}); });
}; };
export const useGetCaCertTemplates = (caId: string) => {
return useQuery({
queryKey: caKeys.getCaCertTemplates(caId),
queryFn: async () => {
const { data } = await apiRequest.get<{
certificateTemplates: TCertificateTemplate[];
}>(`/api/v1/pki/ca/${caId}/certificate-templates`);
return data;
},
enabled: Boolean(caId)
});
};
+3
View File
@@ -19,6 +19,7 @@ export type TCertificateAuthority = {
notAfter?: string; notAfter?: string;
notBefore?: string; notBefore?: string;
keyAlgorithm: CertKeyAlgorithm; keyAlgorithm: CertKeyAlgorithm;
requireTemplateForIssuance: boolean;
activeCaCertId?: string; activeCaCertId?: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
@@ -37,12 +38,14 @@ export type TCreateCaDTO = {
notAfter?: string; notAfter?: string;
maxPathLength: number; maxPathLength: number;
keyAlgorithm: CertKeyAlgorithm; keyAlgorithm: CertKeyAlgorithm;
requireTemplateForIssuance: boolean;
}; };
export type TUpdateCaDTO = { export type TUpdateCaDTO = {
projectSlug: string; projectSlug: string;
caId: string; caId: string;
status?: CaStatus; status?: CaStatus;
requireTemplateForIssuance?: boolean;
}; };
export type TDeleteCaDTO = { export type TDeleteCaDTO = {
@@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { caKeys } from "../ca/queries";
import { workspaceKeys } from "../workspace/queries"; import { workspaceKeys } from "../workspace/queries";
import { certTemplateKeys } from "./queries"; import { certTemplateKeys } from "./queries";
import { import {
@@ -23,8 +24,9 @@ export const useCreateCertTemplate = () => {
); );
return certificateTemplate; return certificateTemplate;
}, },
onSuccess: (_, { projectId }) => { onSuccess: ({ caId }, { projectId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId)); queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId));
queryClient.invalidateQueries(caKeys.getCaCertTemplates(caId));
} }
}); });
}; };
@@ -40,22 +42,25 @@ export const useUpdateCertTemplate = () => {
return certificateTemplate; return certificateTemplate;
}, },
onSuccess: (_, { projectId, id }) => { onSuccess: ({ caId }, { projectId, id }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId)); queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId));
queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id)); queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id));
queryClient.invalidateQueries(caKeys.getCaCertTemplates(caId));
} }
}); });
}; };
export const useDeleteCertTemplate = () => { export const useDeleteCertTemplate = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<void, {}, TDeleteCertificateTemplateDTO>({ return useMutation<TCertificateTemplate, {}, TDeleteCertificateTemplateDTO>({
mutationFn: async (data) => { mutationFn: async (data) => {
return apiRequest.delete(`/api/v1/pki/certificate-templates/${data.id}`); const { data: certificateTemplate } = await apiRequest.delete<TCertificateTemplate>(`/api/v1/pki/certificate-templates/${data.id}`);
return certificateTemplate;
}, },
onSuccess: (_, { projectId, id }) => { onSuccess: ({ caId }, { projectId, id }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId)); queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId));
queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id)); queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id));
queryClient.invalidateQueries(caKeys.getCaCertTemplates(caId));
} }
}); });
}; };
@@ -22,6 +22,7 @@ import { usePopUp } from "@app/hooks/usePopUp";
import { CaModal } from "@app/views/Project/CertificatesPage/components/CaTab/components/CaModal"; import { CaModal } from "@app/views/Project/CertificatesPage/components/CaTab/components/CaModal";
import { CaInstallCertModal } from "../CertificatesPage/components/CaTab/components/CaInstallCertModal"; import { CaInstallCertModal } from "../CertificatesPage/components/CaTab/components/CaInstallCertModal";
import { CertificateTemplatesSection } from "../CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection";
import { import {
CaCertificatesSection, CaCertificatesSection,
CaCrlsSection, CaCrlsSection,
@@ -125,6 +126,7 @@ export const CaPage = withProjectPermission(
</div> </div>
<div className="w-full"> <div className="w-full">
<CaCertificatesSection caId={caId} /> <CaCertificatesSection caId={caId} />
<CertificateTemplatesSection caId={caId} />
<CaCrlsSection caId={caId} /> <CaCrlsSection caId={caId} />
</div> </div>
</div> </div>
@@ -1,4 +1,4 @@
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns"; import { format } from "date-fns";
@@ -33,6 +33,28 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4"> <div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">CA Details</h3> <h3 className="text-lg font-semibold text-mineshaft-100">CA Details</h3>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Identity}>
{(isAllowed) => {
return (
<Tooltip content="Edit 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>