mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-02 21:25:45 +00:00
Merge branch 'ENG-2661' into feat/acme-and-external-ca
This commit is contained in:
@@ -0,0 +1,44 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.Certificate)) {
|
||||||
|
const hasProjectIdColumn = await knex.schema.hasColumn(TableName.Certificate, "projectId");
|
||||||
|
if (!hasProjectIdColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||||
|
t.string("projectId", 36).nullable();
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.raw(`
|
||||||
|
UPDATE "${TableName.Certificate}" cert
|
||||||
|
SET "projectId" = ca."projectId"
|
||||||
|
FROM "${TableName.CertificateAuthority}" ca
|
||||||
|
WHERE cert."caId" = ca.id
|
||||||
|
`);
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||||
|
t.string("projectId").notNullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||||
|
t.uuid("caId").nullable().alter();
|
||||||
|
t.uuid("caCertId").nullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.Certificate)) {
|
||||||
|
if (await knex.schema.hasColumn(TableName.Certificate, "projectId")) {
|
||||||
|
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||||
|
t.dropForeign("projectId");
|
||||||
|
t.dropColumn("projectId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Altering back to notNullable for caId and caCertId will fail
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ export const CertificatesSchema = z.object({
|
|||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
caId: z.string().uuid(),
|
caId: z.string().uuid().nullable().optional(),
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
serialNumber: z.string(),
|
serialNumber: z.string(),
|
||||||
friendlyName: z.string(),
|
friendlyName: z.string(),
|
||||||
@@ -21,10 +21,11 @@ export const CertificatesSchema = z.object({
|
|||||||
revokedAt: z.date().nullable().optional(),
|
revokedAt: z.date().nullable().optional(),
|
||||||
revocationReason: z.number().nullable().optional(),
|
revocationReason: z.number().nullable().optional(),
|
||||||
altNames: z.string().nullable().optional(),
|
altNames: z.string().nullable().optional(),
|
||||||
caCertId: z.string().uuid(),
|
caCertId: z.string().uuid().nullable().optional(),
|
||||||
certificateTemplateId: z.string().uuid().nullable().optional(),
|
certificateTemplateId: z.string().uuid().nullable().optional(),
|
||||||
keyUsages: z.string().array().nullable().optional(),
|
keyUsages: z.string().array().nullable().optional(),
|
||||||
extendedKeyUsages: z.string().array().nullable().optional(),
|
extendedKeyUsages: z.string().array().nullable().optional(),
|
||||||
|
projectId: z.string(),
|
||||||
pkiSubscriberId: z.string().uuid().nullable().optional()
|
pkiSubscriberId: z.string().uuid().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ export enum EventType {
|
|||||||
IMPORT_CA_CERT = "import-certificate-authority-cert",
|
IMPORT_CA_CERT = "import-certificate-authority-cert",
|
||||||
GET_CA_CRLS = "get-certificate-authority-crls",
|
GET_CA_CRLS = "get-certificate-authority-crls",
|
||||||
ISSUE_CERT = "issue-cert",
|
ISSUE_CERT = "issue-cert",
|
||||||
|
IMPORT_CERT = "import-cert",
|
||||||
SIGN_CERT = "sign-cert",
|
SIGN_CERT = "sign-cert",
|
||||||
GET_CA_CERTIFICATE_TEMPLATES = "get-ca-certificate-templates",
|
GET_CA_CERTIFICATE_TEMPLATES = "get-ca-certificate-templates",
|
||||||
GET_CERT = "get-cert",
|
GET_CERT = "get-cert",
|
||||||
@@ -1812,6 +1813,15 @@ interface IssueCert {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ImportCert {
|
||||||
|
type: EventType.IMPORT_CERT;
|
||||||
|
metadata: {
|
||||||
|
certId: string;
|
||||||
|
cn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface SignCert {
|
interface SignCert {
|
||||||
type: EventType.SIGN_CERT;
|
type: EventType.SIGN_CERT;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -2987,6 +2997,7 @@ export type Event =
|
|||||||
| ImportCaCert
|
| ImportCaCert
|
||||||
| GetCaCrls
|
| GetCaCrls
|
||||||
| IssueCert
|
| IssueCert
|
||||||
|
| ImportCert
|
||||||
| SignCert
|
| SignCert
|
||||||
| GetCaCertificateTemplates
|
| GetCaCertificateTemplates
|
||||||
| GetCert
|
| GetCert
|
||||||
|
|||||||
@@ -1669,6 +1669,19 @@ export const CERTIFICATES = {
|
|||||||
certificateChain: "The certificate chain of the certificate.",
|
certificateChain: "The certificate chain of the certificate.",
|
||||||
serialNumberRes: "The serial number of the certificate.",
|
serialNumberRes: "The serial number of the certificate.",
|
||||||
privateKey: "The private key of the certificate."
|
privateKey: "The private key of the certificate."
|
||||||
|
},
|
||||||
|
IMPORT: {
|
||||||
|
projectSlug: "Slug of the project to import the certificate into.",
|
||||||
|
certificatePem: "The PEM-encoded leaf certificate.",
|
||||||
|
privateKeyPem: "The PEM-encoded private key corresponding to the certificate.",
|
||||||
|
chainPem: "The PEM-encoded chain of intermediate certificates.",
|
||||||
|
friendlyName: "A friendly name for the certificate.",
|
||||||
|
pkiCollectionId: "The ID of the PKI collection to add the certificate to.",
|
||||||
|
|
||||||
|
certificate: "The issued certificate.",
|
||||||
|
certificateChain: "The certificate chain of the issued certificate.",
|
||||||
|
privateKey: "The private key of the issued certificate.",
|
||||||
|
serialNumber: "The serial number of the issued certificate."
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -847,7 +847,9 @@ export const registerRoutes = async (
|
|||||||
certificateAuthoritySecretDAL,
|
certificateAuthoritySecretDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
permissionService
|
permissionService,
|
||||||
|
pkiCollectionDAL,
|
||||||
|
pkiCollectionItemDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const sshCertificateAuthorityService = sshCertificateAuthorityServiceFactory({
|
const sshCertificateAuthorityService = sshCertificateAuthorityServiceFactory({
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { cert, ca } = await server.services.certificate.getCert({
|
const { cert } = await server.services.certificate.getCert({
|
||||||
serialNumber: req.params.serialNumber,
|
serialNumber: req.params.serialNumber,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@@ -49,7 +49,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: ca.projectId,
|
projectId: cert.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.GET_CERT,
|
type: EventType.GET_CERT,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -86,7 +86,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req, reply) => {
|
handler: async (req, reply) => {
|
||||||
const { ca, cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({
|
const { cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({
|
||||||
serialNumber: req.params.serialNumber,
|
serialNumber: req.params.serialNumber,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@@ -96,7 +96,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: ca.projectId,
|
projectId: cert.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.GET_CERT_PRIVATE_KEY,
|
type: EventType.GET_CERT_PRIVATE_KEY,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -131,14 +131,14 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
|
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
|
||||||
certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain),
|
certificateChain: z.string().trim().nullable().describe(CERTIFICATES.GET_CERT.certificateChain),
|
||||||
privateKey: z.string().trim().describe(CERTIFICATES.GET_CERT.privateKey),
|
privateKey: z.string().trim().describe(CERTIFICATES.GET_CERT.privateKey),
|
||||||
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
|
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req, reply) => {
|
handler: async (req, reply) => {
|
||||||
const { certificate, certificateChain, serialNumber, cert, ca, privateKey } =
|
const { certificate, certificateChain, serialNumber, cert, privateKey } =
|
||||||
await server.services.certificate.getCertBundle({
|
await server.services.certificate.getCertBundle({
|
||||||
serialNumber: req.params.serialNumber,
|
serialNumber: req.params.serialNumber,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -149,7 +149,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: ca.projectId,
|
projectId: cert.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.GET_CERT_BUNDLE,
|
type: EventType.GET_CERT_BUNDLE,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -284,6 +284,68 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/import-certificate",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.PkiCertificates],
|
||||||
|
description: "Import certificate",
|
||||||
|
body: z.object({
|
||||||
|
projectSlug: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.projectSlug),
|
||||||
|
|
||||||
|
certificatePem: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.certificatePem),
|
||||||
|
privateKeyPem: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.privateKeyPem),
|
||||||
|
chainPem: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.chainPem),
|
||||||
|
|
||||||
|
friendlyName: z.string().trim().optional().describe(CERTIFICATES.IMPORT.friendlyName),
|
||||||
|
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATES.IMPORT.pkiCollectionId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificate: z.string().trim().describe(CERTIFICATES.IMPORT.certificate),
|
||||||
|
certificateChain: z.string().trim().describe(CERTIFICATES.IMPORT.certificateChain),
|
||||||
|
privateKey: z.string().trim().describe(CERTIFICATES.IMPORT.privateKey),
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATES.IMPORT.serialNumber)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificate, certificateChain, privateKey, serialNumber, cert } =
|
||||||
|
await server.services.certificate.importCert({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: cert.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.IMPORT_CERT,
|
||||||
|
metadata: {
|
||||||
|
certId: cert.id,
|
||||||
|
cn: cert.commonName,
|
||||||
|
serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate,
|
||||||
|
certificateChain,
|
||||||
|
privateKey,
|
||||||
|
serialNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/sign-certificate",
|
url: "/sign-certificate",
|
||||||
@@ -474,7 +536,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { deletedCert, ca } = await server.services.certificate.deleteCert({
|
const { deletedCert } = await server.services.certificate.deleteCert({
|
||||||
serialNumber: req.params.serialNumber,
|
serialNumber: req.params.serialNumber,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@@ -484,7 +546,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: ca.projectId,
|
projectId: deletedCert.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.DELETE_CERT,
|
type: EventType.DELETE_CERT,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -518,13 +580,13 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
|
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
|
||||||
certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain),
|
certificateChain: z.string().trim().nullable().describe(CERTIFICATES.GET_CERT.certificateChain),
|
||||||
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
|
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { certificate, certificateChain, serialNumber, cert, ca } = await server.services.certificate.getCertBody({
|
const { certificate, certificateChain, serialNumber, cert } = await server.services.certificate.getCertBody({
|
||||||
serialNumber: req.params.serialNumber,
|
serialNumber: req.params.serialNumber,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@@ -534,7 +596,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: ca.projectId,
|
projectId: cert.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.GET_CERT_BODY,
|
type: EventType.GET_CERT_BODY,
|
||||||
metadata: {
|
metadata: {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import * as x509 from "@peculiar/x509";
|
|||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { getProjectKmsCertificateKeyId } from "../project/project-fns";
|
import { getProjectKmsCertificateKeyId } from "../project/project-fns";
|
||||||
import { CrlReason, TBuildCertificateChainDTO, TGetCertificateCredentialsDTO } from "./certificate-types";
|
import { CrlReason, TGetCertificateCredentialsDTO } from "./certificate-types";
|
||||||
|
|
||||||
export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
|
export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
|
||||||
switch (crlReason) {
|
switch (crlReason) {
|
||||||
@@ -52,6 +52,9 @@ export const constructPemChainFromCerts = (certificates: x509.X509Certificate[])
|
|||||||
.join("\n")
|
.join("\n")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
|
export const splitPemChain = (pemText: string) =>
|
||||||
|
pemText.match(/-----BEGIN CERTIFICATE-----[^-]+-----END CERTIFICATE-----/g) || [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the public and private key of certificate
|
* Return the public and private key of certificate
|
||||||
* Note: credentials are returned as PEM strings
|
* Note: credentials are returned as PEM strings
|
||||||
@@ -95,29 +98,3 @@ export const getCertificateCredentials = async ({
|
|||||||
throw new BadRequestError({ message: `Failed to process private key for certificate with ID '${certId}'` });
|
throw new BadRequestError({ message: `Failed to process private key for certificate with ID '${certId}'` });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the certificate was generated after ~05/01/25 it will have a encryptedCertificateChain attached to it's body
|
|
||||||
// Otherwise we'll fallback to manually building the chain
|
|
||||||
export const buildCertificateChain = async ({
|
|
||||||
caCert,
|
|
||||||
caCertChain,
|
|
||||||
encryptedCertificateChain,
|
|
||||||
kmsService,
|
|
||||||
kmsId
|
|
||||||
}: TBuildCertificateChainDTO) => {
|
|
||||||
if (!encryptedCertificateChain && (!caCert || !caCertChain)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let certificateChain = `${caCert}\n${caCertChain}`.trim();
|
|
||||||
|
|
||||||
if (encryptedCertificateChain) {
|
|
||||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({ kmsId });
|
|
||||||
const decryptedCertChain = await kmsDecryptor({
|
|
||||||
cipherTextBlob: encryptedCertificateChain
|
|
||||||
});
|
|
||||||
certificateChain = decryptedCertChain.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return certificateChain;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import * as x509 from "@peculiar/x509";
|
import * as x509 from "@peculiar/x509";
|
||||||
|
import { createPrivateKey, createPublicKey, sign, verify } from "crypto";
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
||||||
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionCertificateActions,
|
ProjectPermissionCertificateActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
|
||||||
|
import { TPkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
@@ -21,12 +25,16 @@ import { expandInternalCa, getCaCertChain, rebuildCaCrl } from "../certificate-a
|
|||||||
import { buildCertificateChain, getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns";
|
import { buildCertificateChain, getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns";
|
||||||
import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
|
import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
|
||||||
import {
|
import {
|
||||||
|
CertExtendedKeyUsage,
|
||||||
|
CertExtendedKeyUsageOIDToName,
|
||||||
|
CertKeyUsage,
|
||||||
CertStatus,
|
CertStatus,
|
||||||
TDeleteCertDTO,
|
TDeleteCertDTO,
|
||||||
TGetCertBodyDTO,
|
TGetCertBodyDTO,
|
||||||
TGetCertBundleDTO,
|
TGetCertBundleDTO,
|
||||||
TGetCertDTO,
|
TGetCertDTO,
|
||||||
TGetCertPrivateKeyDTO,
|
TGetCertPrivateKeyDTO,
|
||||||
|
TImportCertDTO,
|
||||||
TRevokeCertDTO
|
TRevokeCertDTO
|
||||||
} from "./certificate-types";
|
} from "./certificate-types";
|
||||||
|
|
||||||
@@ -38,7 +46,12 @@ type TCertificateServiceFactoryDep = {
|
|||||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
|
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
|
||||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "findById" | "transaction">;
|
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
|
||||||
|
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
|
||||||
|
projectDAL: Pick<
|
||||||
|
TProjectDALFactory,
|
||||||
|
"findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction" | "getProjectFromSplitId"
|
||||||
|
>;
|
||||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
};
|
};
|
||||||
@@ -53,6 +66,8 @@ export const certificateServiceFactory = ({
|
|||||||
certificateAuthorityCertDAL,
|
certificateAuthorityCertDAL,
|
||||||
certificateAuthorityCrlDAL,
|
certificateAuthorityCrlDAL,
|
||||||
certificateAuthoritySecretDAL,
|
certificateAuthoritySecretDAL,
|
||||||
|
pkiCollectionDAL,
|
||||||
|
pkiCollectionItemDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
permissionService
|
permissionService
|
||||||
@@ -67,7 +82,7 @@ export const certificateServiceFactory = ({
|
|||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId: ca.projectId,
|
projectId: cert.projectId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
@@ -100,7 +115,7 @@ export const certificateServiceFactory = ({
|
|||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId: ca.projectId,
|
projectId: cert.projectId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
@@ -113,7 +128,7 @@ export const certificateServiceFactory = ({
|
|||||||
|
|
||||||
const { certPrivateKey } = await getCertificateCredentials({
|
const { certPrivateKey } = await getCertificateCredentials({
|
||||||
certId: cert.id,
|
certId: cert.id,
|
||||||
projectId: ca.projectId,
|
projectId: cert.projectId,
|
||||||
certificateSecretDAL,
|
certificateSecretDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
kmsService
|
kmsService
|
||||||
@@ -136,7 +151,7 @@ export const certificateServiceFactory = ({
|
|||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId: ca.projectId,
|
projectId: cert.projectId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
@@ -224,7 +239,7 @@ export const certificateServiceFactory = ({
|
|||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId: ca.projectId,
|
projectId: cert.projectId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
@@ -238,7 +253,7 @@ export const certificateServiceFactory = ({
|
|||||||
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||||
|
|
||||||
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
||||||
projectId: ca.projectId,
|
projectId: cert.projectId,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
@@ -252,21 +267,26 @@ export const certificateServiceFactory = ({
|
|||||||
|
|
||||||
const certObj = new x509.X509Certificate(decryptedCert);
|
const certObj = new x509.X509Certificate(decryptedCert);
|
||||||
|
|
||||||
const { caCert, caCertChain } = await getCaCertChain({
|
let certificateChain = null;
|
||||||
caCertId: cert.caCertId,
|
|
||||||
certificateAuthorityDAL,
|
|
||||||
certificateAuthorityCertDAL,
|
|
||||||
projectDAL,
|
|
||||||
kmsService
|
|
||||||
});
|
|
||||||
|
|
||||||
const certificateChain = await buildCertificateChain({
|
// On newer certs the certBody.encryptedCertificateChain column will always exist.
|
||||||
caCert,
|
// Older certs will have a caCertId which will be used as a fallback mechanism for structuring the chain.
|
||||||
caCertChain,
|
if (certBody.encryptedCertificateChain) {
|
||||||
kmsId: certificateManagerKeyId,
|
const decryptedCertChain = await kmsDecryptor({
|
||||||
kmsService,
|
cipherTextBlob: certBody.encryptedCertificateChain
|
||||||
encryptedCertificateChain: certBody.encryptedCertificateChain || undefined
|
});
|
||||||
});
|
certificateChain = decryptedCertChain.toString();
|
||||||
|
} else if (cert.caCertId) {
|
||||||
|
const { caCert, caCertChain } = await getCaCertChain({
|
||||||
|
caCertId: cert.caCertId,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
certificateChain = `${caCert}\n${caCertChain}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificate: certObj.toString("pem"),
|
certificate: certObj.toString("pem"),
|
||||||
@@ -288,7 +308,7 @@ export const certificateServiceFactory = ({
|
|||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId: ca.projectId,
|
projectId: cert.projectId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
@@ -306,7 +326,7 @@ export const certificateServiceFactory = ({
|
|||||||
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||||
|
|
||||||
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
||||||
projectId: ca.projectId,
|
projectId: cert.projectId,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
@@ -321,25 +341,30 @@ export const certificateServiceFactory = ({
|
|||||||
const certObj = new x509.X509Certificate(decryptedCert);
|
const certObj = new x509.X509Certificate(decryptedCert);
|
||||||
const certificate = certObj.toString("pem");
|
const certificate = certObj.toString("pem");
|
||||||
|
|
||||||
const { caCert, caCertChain } = await getCaCertChain({
|
let certificateChain = null;
|
||||||
caCertId: cert.caCertId,
|
|
||||||
certificateAuthorityDAL,
|
|
||||||
certificateAuthorityCertDAL,
|
|
||||||
projectDAL,
|
|
||||||
kmsService
|
|
||||||
});
|
|
||||||
|
|
||||||
const certificateChain = await buildCertificateChain({
|
// On newer certs the certBody.encryptedCertificateChain column will always exist.
|
||||||
caCert,
|
// Older certs will have a caCertId which will be used as a fallback mechanism for structuring the chain.
|
||||||
caCertChain,
|
if (certBody.encryptedCertificateChain) {
|
||||||
kmsId: certificateManagerKeyId,
|
const decryptedCertChain = await kmsDecryptor({
|
||||||
kmsService,
|
cipherTextBlob: certBody.encryptedCertificateChain
|
||||||
encryptedCertificateChain: certBody.encryptedCertificateChain || undefined
|
});
|
||||||
});
|
certificateChain = decryptedCertChain.toString();
|
||||||
|
} else if (cert.caCertId) {
|
||||||
|
const { caCert, caCertChain } = await getCaCertChain({
|
||||||
|
caCertId: cert.caCertId,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
certificateChain = `${caCert}\n${caCertChain}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
const { certPrivateKey } = await getCertificateCredentials({
|
const { certPrivateKey } = await getCertificateCredentials({
|
||||||
certId: cert.id,
|
certId: cert.id,
|
||||||
projectId: ca.projectId,
|
projectId: cert.projectId,
|
||||||
certificateSecretDAL,
|
certificateSecretDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
kmsService
|
kmsService
|
||||||
@@ -361,6 +386,7 @@ export const certificateServiceFactory = ({
|
|||||||
deleteCert,
|
deleteCert,
|
||||||
revokeCert,
|
revokeCert,
|
||||||
getCertBody,
|
getCertBody,
|
||||||
|
importCert,
|
||||||
getCertBundle
|
getCertBundle
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -78,6 +78,17 @@ export type TGetCertBodyDTO = {
|
|||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TImportCertDTO = {
|
||||||
|
projectSlug: string;
|
||||||
|
|
||||||
|
friendlyName?: string;
|
||||||
|
pkiCollectionId?: string;
|
||||||
|
|
||||||
|
certificatePem: string;
|
||||||
|
privateKeyPem: string;
|
||||||
|
chainPem: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TGetCertPrivateKeyDTO = {
|
export type TGetCertPrivateKeyDTO = {
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
@@ -93,11 +104,3 @@ export type TGetCertificateCredentialsDTO = {
|
|||||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TBuildCertificateChainDTO = {
|
|
||||||
caCert?: string;
|
|
||||||
caCertChain?: string;
|
|
||||||
encryptedCertificateChain?: Buffer;
|
|
||||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey">;
|
|
||||||
kmsId: string;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -269,14 +269,8 @@ export const pkiCollectionServiceFactory = ({
|
|||||||
});
|
});
|
||||||
if (isCertAdded) throw new BadRequestError({ message: "Certificate already part of the PKI collection" });
|
if (isCertAdded) throw new BadRequestError({ message: "Certificate already part of the PKI collection" });
|
||||||
|
|
||||||
// validate that there exists a certificate in same project as PKI collection
|
|
||||||
const cas = await certificateAuthorityDAL.find({ projectId: pkiCollection.projectId });
|
|
||||||
|
|
||||||
// TODO: consider making this more efficient
|
|
||||||
const [certificate] = await certificateDAL.find({
|
const [certificate] = await certificateDAL.find({
|
||||||
$in: {
|
projectId: pkiCollection.projectId,
|
||||||
caId: cas.map((ca) => ca.id)
|
|
||||||
},
|
|
||||||
id: itemId
|
id: itemId
|
||||||
});
|
});
|
||||||
if (!certificate) throw new NotFoundError({ message: `Certificate with ID '${itemId}' not found` });
|
if (!certificate) throw new NotFoundError({ message: `Certificate with ID '${itemId}' not found` });
|
||||||
|
|||||||
@@ -611,7 +611,8 @@ export const pkiSubscriberServiceFactory = ({
|
|||||||
notBefore: notBeforeDate,
|
notBefore: notBeforeDate,
|
||||||
notAfter: notAfterDate,
|
notAfter: notAfterDate,
|
||||||
keyUsages: selectedKeyUsages,
|
keyUsages: selectedKeyUsages,
|
||||||
extendedKeyUsages: selectedExtendedKeyUsages
|
extendedKeyUsages: selectedExtendedKeyUsages,
|
||||||
|
projectId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -969,13 +969,9 @@ export const projectServiceFactory = ({
|
|||||||
ProjectPermissionSub.Certificates
|
ProjectPermissionSub.Certificates
|
||||||
);
|
);
|
||||||
|
|
||||||
const cas = await certificateAuthorityDAL.find({ projectId });
|
|
||||||
|
|
||||||
const certificates = await certificateDAL.find(
|
const certificates = await certificateDAL.find(
|
||||||
{
|
{
|
||||||
$in: {
|
projectId,
|
||||||
caId: cas.map((ca) => ca.id)
|
|
||||||
},
|
|
||||||
...(friendlyName && { friendlyName }),
|
...(friendlyName && { friendlyName }),
|
||||||
...(commonName && { commonName })
|
...(commonName && { commonName })
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
|||||||
[EventType.IMPORT_CA_CERT]: "Import CA certificate",
|
[EventType.IMPORT_CA_CERT]: "Import CA certificate",
|
||||||
[EventType.GET_CA_CRL]: "Get CA CRL",
|
[EventType.GET_CA_CRL]: "Get CA CRL",
|
||||||
[EventType.ISSUE_CERT]: "Issue certificate",
|
[EventType.ISSUE_CERT]: "Issue certificate",
|
||||||
|
[EventType.IMPORT_CERT]: "Import certificate",
|
||||||
[EventType.GET_CERT]: "Get certificate",
|
[EventType.GET_CERT]: "Get certificate",
|
||||||
[EventType.DELETE_CERT]: "Delete certificate",
|
[EventType.DELETE_CERT]: "Delete certificate",
|
||||||
[EventType.REVOKE_CERT]: "Revoke certificate",
|
[EventType.REVOKE_CERT]: "Revoke certificate",
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export enum EventType {
|
|||||||
IMPORT_CA_CERT = "import-certificate-authority-cert",
|
IMPORT_CA_CERT = "import-certificate-authority-cert",
|
||||||
GET_CA_CRL = "get-certificate-authority-crl",
|
GET_CA_CRL = "get-certificate-authority-crl",
|
||||||
ISSUE_CERT = "issue-cert",
|
ISSUE_CERT = "issue-cert",
|
||||||
|
IMPORT_CERT = "import-cert",
|
||||||
GET_CERT = "get-cert",
|
GET_CERT = "get-cert",
|
||||||
DELETE_CERT = "delete-cert",
|
DELETE_CERT = "delete-cert",
|
||||||
REVOKE_CERT = "revoke-cert",
|
REVOKE_CERT = "revoke-cert",
|
||||||
|
|||||||
@@ -583,6 +583,14 @@ interface IssueCert {
|
|||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
interface ImportCert {
|
||||||
|
type: EventType.IMPORT_CERT;
|
||||||
|
metadata: {
|
||||||
|
certId: string;
|
||||||
|
cn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface GetCert {
|
interface GetCert {
|
||||||
type: EventType.GET_CERT;
|
type: EventType.GET_CERT;
|
||||||
@@ -895,6 +903,7 @@ export type Event =
|
|||||||
| ImportCaCert
|
| ImportCaCert
|
||||||
| GetCaCrl
|
| GetCaCrl
|
||||||
| IssueCert
|
| IssueCert
|
||||||
|
| ImportCert
|
||||||
| GetCert
|
| GetCert
|
||||||
| DeleteCert
|
| DeleteCert
|
||||||
| RevokeCert
|
| RevokeCert
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export { useDeleteCert, useRevokeCert } from "./mutations";
|
export { useDeleteCert, useImportCertificate, useRevokeCert } from "./mutations";
|
||||||
export { useGetCert, useGetCertBody } from "./queries";
|
export { useGetCert, useGetCertBody } from "./queries";
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import { apiRequest } from "@app/config/request";
|
|||||||
|
|
||||||
import { pkiSubscriberKeys } from "../pkiSubscriber/queries";
|
import { pkiSubscriberKeys } from "../pkiSubscriber/queries";
|
||||||
import { workspaceKeys } from "../workspace";
|
import { workspaceKeys } from "../workspace";
|
||||||
import { TCertificate, TDeleteCertDTO, TRevokeCertDTO } from "./types";
|
import {
|
||||||
|
TCertificate,
|
||||||
|
TDeleteCertDTO,
|
||||||
|
TImportCertificateDTO,
|
||||||
|
TImportCertificateResponse,
|
||||||
|
TRevokeCertDTO
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export const useDeleteCert = () => {
|
export const useDeleteCert = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -49,3 +55,21 @@ export const useRevokeCert = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useImportCertificate = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TImportCertificateResponse, object, TImportCertificateDTO>({
|
||||||
|
mutationFn: async (body) => {
|
||||||
|
const { data } = await apiRequest.post<TImportCertificateResponse>(
|
||||||
|
"/api/v1/pki/certificates/import-certificate",
|
||||||
|
body
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectSlug }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: workspaceKeys.forWorkspaceCertificates(projectSlug)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -25,3 +25,21 @@ export type TRevokeCertDTO = {
|
|||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
revocationReason: string;
|
revocationReason: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TImportCertificateDTO = {
|
||||||
|
projectSlug: string;
|
||||||
|
|
||||||
|
certificatePem: string;
|
||||||
|
privateKeyPem: string;
|
||||||
|
chainPem: string;
|
||||||
|
|
||||||
|
pkiCollectionId?: string;
|
||||||
|
friendlyName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TImportCertificateResponse = {
|
||||||
|
certificate: string;
|
||||||
|
certificateChain: string;
|
||||||
|
privateKey: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
|||||||
+244
@@ -0,0 +1,244 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
TextArea
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useGetCert, useImportCertificate, useListWorkspacePkiCollections } from "@app/hooks/api";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { CertificateContent } from "./CertificateContent";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
certificatePem: z.string().trim().min(1, "Certificate PEM is required"),
|
||||||
|
privateKeyPem: z.string().trim().min(1, "Private Key PEM is required"),
|
||||||
|
chainPem: z.string().trim().min(1, "Certificate Chain PEM is required"),
|
||||||
|
|
||||||
|
friendlyName: z.string(),
|
||||||
|
collectionId: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
popUp: UsePopUpState<["certificateImport"]>;
|
||||||
|
handlePopUpToggle: (
|
||||||
|
popUpName: keyof UsePopUpState<["certificateImport"]>,
|
||||||
|
state?: boolean
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TCertificateDetails = {
|
||||||
|
serialNumber: string;
|
||||||
|
certificate: string;
|
||||||
|
certificateChain: string;
|
||||||
|
privateKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CertificateImportModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
|
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { data: cert } = useGetCert(
|
||||||
|
(popUp?.certificateImport?.data as { serialNumber: string })?.serialNumber || ""
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data } = useListWorkspacePkiCollections({
|
||||||
|
workspaceId: currentWorkspace?.id || ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: importCertificate } = useImportCertificate();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema)
|
||||||
|
});
|
||||||
|
|
||||||
|
const onFormSubmit = async ({
|
||||||
|
certificatePem,
|
||||||
|
privateKeyPem,
|
||||||
|
chainPem,
|
||||||
|
friendlyName,
|
||||||
|
collectionId
|
||||||
|
}: FormData) => {
|
||||||
|
try {
|
||||||
|
if (!currentWorkspace?.slug) return;
|
||||||
|
|
||||||
|
const { serialNumber, certificate, certificateChain, privateKey } = await importCertificate({
|
||||||
|
projectSlug: currentWorkspace.slug,
|
||||||
|
|
||||||
|
certificatePem,
|
||||||
|
privateKeyPem,
|
||||||
|
chainPem,
|
||||||
|
|
||||||
|
friendlyName,
|
||||||
|
pkiCollectionId: collectionId
|
||||||
|
});
|
||||||
|
|
||||||
|
reset();
|
||||||
|
|
||||||
|
setCertificateDetails({
|
||||||
|
serialNumber,
|
||||||
|
certificate,
|
||||||
|
certificateChain,
|
||||||
|
privateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully imported certificate",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to import certificate",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.certificateImport?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("certificateImport", isOpen);
|
||||||
|
reset();
|
||||||
|
setCertificateDetails(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent title={`${cert ? "View" : "Import"} Certificate`}>
|
||||||
|
{!certificateDetails ? (
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="collectionId"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Certificate Collection"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
isOptional
|
||||||
|
className="mt-4"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
isDisabled={Boolean(cert)}
|
||||||
|
>
|
||||||
|
{(data?.collections || []).map(({ id, name }) => (
|
||||||
|
<SelectItem value={id} key={`pki-collection-${id}`}>
|
||||||
|
{name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="friendlyName"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Friendly Name"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="My Certificate" isDisabled={Boolean(cert)} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="certificatePem"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Leaf Certificate PEM"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<TextArea {...field} isDisabled={Boolean(cert)} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="privateKeyPem"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Private Key PEM"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<TextArea {...field} isDisabled={Boolean(cert)} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="chainPem"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Certificate Chain PEM"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<TextArea {...field} isDisabled={Boolean(cert)} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{!cert && (
|
||||||
|
<div className="mt-4 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("certificateImport", false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<CertificateContent
|
||||||
|
serialNumber={certificateDetails.serialNumber}
|
||||||
|
certificate={certificateDetails.certificate}
|
||||||
|
certificateChain={certificateDetails.certificateChain}
|
||||||
|
privateKey={certificateDetails.privateKey}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
+23
-10
@@ -1,4 +1,4 @@
|
|||||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faArrowRight, 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";
|
||||||
@@ -13,6 +13,7 @@ import { useDeleteCert } from "@app/hooks/api";
|
|||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { CertificateCertModal } from "./CertificateCertModal";
|
import { CertificateCertModal } from "./CertificateCertModal";
|
||||||
|
import { CertificateImportModal } from "./CertificateImportModal";
|
||||||
import { CertificateModal } from "./CertificateModal";
|
import { CertificateModal } from "./CertificateModal";
|
||||||
import { CertificateRevocationModal } from "./CertificateRevocationModal";
|
import { CertificateRevocationModal } from "./CertificateRevocationModal";
|
||||||
import { CertificatesTable } from "./CertificatesTable";
|
import { CertificatesTable } from "./CertificatesTable";
|
||||||
@@ -23,6 +24,7 @@ export const CertificatesSection = () => {
|
|||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"certificate",
|
"certificate",
|
||||||
|
"certificateImport",
|
||||||
"certificateCert",
|
"certificateCert",
|
||||||
"deleteCertificate",
|
"deleteCertificate",
|
||||||
"revokeCertificate"
|
"revokeCertificate"
|
||||||
@@ -58,20 +60,31 @@ export const CertificatesSection = () => {
|
|||||||
a={ProjectPermissionSub.Certificates}
|
a={ProjectPermissionSub.Certificates}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<div className="flex gap-2">
|
||||||
colorSchema="primary"
|
<Button
|
||||||
type="submit"
|
variant="outline_bg"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faArrowRight} />}
|
||||||
onClick={() => handlePopUpOpen("certificate")}
|
onClick={() => handlePopUpOpen("certificateImport")}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Issue
|
Import
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
type="submit"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => handlePopUpOpen("certificate")}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Issue
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
<CertificatesTable handlePopUpOpen={handlePopUpOpen} />
|
<CertificatesTable handlePopUpOpen={handlePopUpOpen} />
|
||||||
<CertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<CertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
<CertificateImportModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
<CertificateCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<CertificateCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
|
|||||||
Reference in New Issue
Block a user