Merge branch 'certificate-alerting' into feature/certificate-template

This commit is contained in:
Sheen Capadngan
2024-08-16 18:42:12 +08:00
24 changed files with 255 additions and 83 deletions
@@ -4,16 +4,19 @@ import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> { export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthority)) { if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
const hasActiveCaCertVersionColumn = await knex.schema.hasColumn( const hasActiveCaCertIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertId");
TableName.CertificateAuthority, if (!hasActiveCaCertIdColumn) {
"activeCaCertVersion"
);
if (!hasActiveCaCertVersionColumn) {
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => { await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.integer("activeCaCertVersion").nullable(); t.uuid("activeCaCertId").nullable();
t.foreign("activeCaCertId").references("id").inTable(TableName.CertificateAuthorityCert);
}); });
await knex(TableName.CertificateAuthority).where("status", "active").update({ activeCaCertVersion: 1 }); await knex.raw(`
UPDATE "${TableName.CertificateAuthority}" ca
SET "activeCaCertId" = cac.id
FROM "${TableName.CertificateAuthorityCert}" cac
WHERE ca.id = cac."caId"
`);
} }
} }
@@ -54,18 +57,38 @@ export async function up(knex: Knex): Promise<void> {
} }
} }
// if (await knex.schema.hasTable(TableName.CertificateAuthoritySecret)) { if (await knex.schema.hasTable(TableName.CertificateAuthoritySecret)) {
// await knex.schema.alterTable(TableName.CertificateAuthoritySecret, (t) => { await knex.schema.alterTable(TableName.CertificateAuthoritySecret, (t) => {
// t.dropUnique(["caId"]); t.dropUnique(["caId"]);
// }); });
// } }
if (await knex.schema.hasTable(TableName.Certificate)) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.uuid("caCertId").nullable();
t.foreign("caCertId").references("id").inTable(TableName.CertificateAuthorityCert);
});
await knex.raw(`
UPDATE "${TableName.Certificate}" cert
SET "caCertId" = (
SELECT caCert.id
FROM "${TableName.CertificateAuthorityCert}" caCert
WHERE caCert."caId" = cert."caId"
)
`);
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.uuid("caCertId").notNullable().alter();
});
}
} }
export async function down(knex: Knex): Promise<void> { export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthority)) { if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
if (await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertVersion")) { if (await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertId")) {
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => { await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.dropColumn("activeCaCertVersion"); t.dropColumn("activeCaCertId");
}); });
} }
} }
@@ -83,4 +106,12 @@ export async function down(knex: Knex): Promise<void> {
}); });
} }
} }
if (await knex.schema.hasTable(TableName.Certificate)) {
if (await knex.schema.hasColumn(TableName.Certificate, "caCertId")) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.dropColumn("caCertId");
});
}
}
} }
@@ -11,6 +11,7 @@ export async function up(knex: Knex): Promise<void> {
t.string("projectId").notNullable(); t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.string("name").notNullable(); t.string("name").notNullable();
t.string("description").notNullable();
}); });
} }
@@ -28,7 +28,7 @@ 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(),
activeCaCertVersion: z.number().nullable().optional() activeCaCertId: z.string().uuid().nullable().optional()
}); });
export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>; export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>;
+2 -1
View File
@@ -20,7 +20,8 @@ export const CertificatesSchema = z.object({
notAfter: z.date(), notAfter: z.date(),
revokedAt: z.date().nullable().optional(), revokedAt: z.date().nullable().optional(),
revocationReason: z.number().nullable().optional(), revocationReason: z.number().nullable().optional(),
altNames: z.string().default("").nullable().optional() altNames: z.string().default("").nullable().optional(),
caCertId: z.string().uuid()
}); });
export type TCertificates = z.infer<typeof CertificatesSchema>; export type TCertificates = z.infer<typeof CertificatesSchema>;
+2 -1
View File
@@ -12,7 +12,8 @@ export const PkiCollectionsSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
projectId: z.string(), projectId: z.string(),
name: z.string() name: z.string(),
description: z.string()
}); });
export type TPkiCollections = z.infer<typeof PkiCollectionsSchema>; export type TPkiCollections = z.infer<typeof PkiCollectionsSchema>;
+6 -2
View File
@@ -1089,6 +1089,7 @@ export const CERTIFICATE_AUTHORITIES = {
ISSUE_CERT: { ISSUE_CERT: {
caId: "The ID of the CA to issue the certificate from", caId: "The ID of the CA to issue the certificate from",
certificateTemplateId: "The ID of the certificate template to issue the certificate from", certificateTemplateId: "The ID of the certificate template to issue the certificate from",
pkiCollectionId: "The ID of the PKI collection to add the certificate to",
friendlyName: "A friendly name for the certificate", friendlyName: "A friendly name for the certificate",
commonName: "The common name (CN) for the certificate", commonName: "The common name (CN) for the certificate",
altNames: altNames:
@@ -1104,6 +1105,7 @@ export const CERTIFICATE_AUTHORITIES = {
}, },
SIGN_CERT: { SIGN_CERT: {
caId: "The ID of the CA to issue the certificate from", caId: "The ID of the CA to issue the certificate from",
pkiCollectionId: "The ID of the PKI collection to add the certificate to",
csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance", csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance",
friendlyName: "A friendly name for the certificate", friendlyName: "A friendly name for the certificate",
commonName: "The common name (CN) for the certificate", commonName: "The common name (CN) for the certificate",
@@ -1171,14 +1173,16 @@ export const ALERTS = {
export const PKI_COLLECTIONS = { export const PKI_COLLECTIONS = {
CREATE: { CREATE: {
projectId: "The ID of the project to create the PKI collection in", projectId: "The ID of the project to create the PKI collection in",
name: "The name of the PKI collection" name: "The name of the PKI collection",
description: "A description for the PKI collection"
}, },
GET: { GET: {
collectionId: "The ID of the PKI collection to get" collectionId: "The ID of the PKI collection to get"
}, },
UPDATE: { UPDATE: {
collectionId: "The ID of the PKI collection to update", collectionId: "The ID of the PKI collection to update",
name: "The name of the PKI collection to update to" name: "The name of the PKI collection to update to",
description: "The description for the PKI collection to update to"
}, },
DELETE: { DELETE: {
collectionId: "The ID of the PKI collection to delete" collectionId: "The ID of the PKI collection to delete"
+2
View File
@@ -626,6 +626,8 @@ export const registerRoutes = async (
certificateAuthorityQueue, certificateAuthorityQueue,
certificateDAL, certificateDAL,
certificateBodyDAL, certificateBodyDAL,
pkiCollectionDAL,
pkiCollectionItemDAL,
projectDAL, projectDAL,
kmsService, kmsService,
permissionService permissionService
@@ -283,7 +283,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Renew CA certificate for CA", description: "Perform CA certificate renewal",
params: z.object({ params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.caId) caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.caId)
}), }),
@@ -556,6 +556,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}), }),
body: z body: z
.object({ .object({
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.pkiCollectionId),
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName), friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName), commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames), altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
@@ -635,6 +636,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
body: z body: z
.object({ .object({
csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.csr), csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.csr),
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.pkiCollectionId),
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.friendlyName), friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.friendlyName),
commonName: z.string().trim().min(1).optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.commonName), commonName: z.string().trim().min(1).optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.commonName),
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.altNames), altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.altNames),
@@ -20,7 +20,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) =>
description: "Create PKI collection", description: "Create PKI collection",
body: z.object({ body: z.object({
projectId: z.string().trim().describe(PKI_COLLECTIONS.CREATE.projectId), projectId: z.string().trim().describe(PKI_COLLECTIONS.CREATE.projectId),
name: z.string().trim().describe(PKI_COLLECTIONS.CREATE.name) name: z.string().trim().describe(PKI_COLLECTIONS.CREATE.name),
description: z.string().trim().default("").describe(PKI_COLLECTIONS.CREATE.description)
}), }),
response: { response: {
200: PkiCollectionsSchema 200: PkiCollectionsSchema
@@ -104,7 +105,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) =>
collectionId: z.string().trim().describe(PKI_COLLECTIONS.UPDATE.collectionId) collectionId: z.string().trim().describe(PKI_COLLECTIONS.UPDATE.collectionId)
}), }),
body: z.object({ body: z.object({
name: z.string().trim().optional().describe(PKI_COLLECTIONS.UPDATE.name) name: z.string().trim().optional().describe(PKI_COLLECTIONS.UPDATE.name),
description: z.string().trim().optional().describe(PKI_COLLECTIONS.UPDATE.description)
}), }),
response: { response: {
200: PkiCollectionsSchema 200: PkiCollectionsSchema
@@ -195,19 +195,18 @@ export const getCaCertChains = async ({
/** /**
* Return the decrypted pem-encoded certificate and certificate chain * Return the decrypted pem-encoded certificate and certificate chain
* for CA with id [caId]. * corresponding to CA certificate with id [caCertId].
*/ */
export const getCaCertChain = async ({ export const getCaCertChain = async ({
caId, caCertId,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
projectDAL, projectDAL,
kmsService kmsService
}: TGetCaCertChainDTO) => { }: TGetCaCertChainDTO) => {
const ca = await certificateAuthorityDAL.findById(caId); const caCert = await certificateAuthorityCertDAL.findById(caCertId);
if (!ca) throw new BadRequestError({ message: "CA not found" }); if (!caCert) throw new BadRequestError({ message: "CA certificate not found" });
const ca = await certificateAuthorityDAL.findById(caCert.caId);
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
const keyId = await getProjectKmsCertificateKeyId({ const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId, projectId: ca.projectId,
@@ -12,6 +12,8 @@ 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 { 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";
@@ -54,13 +56,18 @@ type TCertificateAuthorityServiceFactoryDep = {
TCertificateAuthorityDALFactory, TCertificateAuthorityDALFactory,
"transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" "transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne"
>; >;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "create" | "findOne" | "transaction" | "find">; certificateAuthorityCertDAL: Pick<
TCertificateAuthorityCertDALFactory,
"create" | "findOne" | "transaction" | "find" | "findById"
>;
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">;
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">;
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">; kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
@@ -76,6 +83,8 @@ export const certificateAuthorityServiceFactory = ({
certificateTemplateDAL, certificateTemplateDAL,
certificateDAL, certificateDAL,
certificateBodyDAL, certificateBodyDAL,
pkiCollectionDAL,
pkiCollectionItemDAL,
projectDAL, projectDAL,
kmsService, kmsService,
permissionService permissionService
@@ -158,8 +167,7 @@ export const certificateAuthorityServiceFactory = ({
maxPathLength, maxPathLength,
notBefore: notBeforeDate, notBefore: notBeforeDate,
notAfter: notAfterDate, notAfter: notAfterDate,
serialNumber, serialNumber
activeCaCertVersion: 1
}) })
}, },
tx tx
@@ -218,7 +226,7 @@ export const certificateAuthorityServiceFactory = ({
plainText: Buffer.alloc(0) plainText: Buffer.alloc(0)
}); });
await certificateAuthorityCertDAL.create( const caCert = await certificateAuthorityCertDAL.create(
{ {
caId: ca.id, caId: ca.id,
encryptedCertificate, encryptedCertificate,
@@ -228,6 +236,14 @@ export const certificateAuthorityServiceFactory = ({
}, },
tx tx
); );
await certificateAuthorityDAL.updateById(
ca.id,
{
activeCaCertId: caCert.id
},
tx
);
} }
// create empty CRL // create empty CRL
@@ -352,9 +368,7 @@ export const certificateAuthorityServiceFactory = ({
); );
if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" }); if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" });
if (ca.activeCaCertId) throw new BadRequestError({ message: "CA already has a certificate installed" });
const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] });
if (caCert) throw new BadRequestError({ message: "CA already has a certificate installed" });
const { caPrivateKey, caPublicKey } = await getCaCredentials({ const { caPrivateKey, caPublicKey } = await getCaCredentials({
caId, caId,
@@ -399,6 +413,8 @@ export const certificateAuthorityServiceFactory = ({
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" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -415,8 +431,7 @@ 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" });
// get latest CA certificate // get latest CA certificate
const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const serialNumber = crypto.randomBytes(32).toString("hex"); const serialNumber = crypto.randomBytes(32).toString("hex");
@@ -494,13 +509,12 @@ export const certificateAuthorityServiceFactory = ({
}); });
await certificateAuthorityDAL.transaction(async (tx) => { await certificateAuthorityDAL.transaction(async (tx) => {
const newActiveCaCertVersion = caCert.version + 1; const newCaCert = await certificateAuthorityCertDAL.create(
await certificateAuthorityCertDAL.create(
{ {
caId: ca.id, caId: ca.id,
encryptedCertificate, encryptedCertificate,
encryptedCertificateChain, encryptedCertificateChain,
version: newActiveCaCertVersion, version: caCert.version + 1,
caSecretId: caSecret.id caSecretId: caSecret.id
}, },
tx tx
@@ -509,7 +523,7 @@ export const certificateAuthorityServiceFactory = ({
await certificateAuthorityDAL.updateById( await certificateAuthorityDAL.updateById(
ca.id, ca.id,
{ {
activeCaCertVersion: newActiveCaCertVersion, activeCaCertId: newCaCert.id,
notBefore: notBeforeDate, notBefore: notBeforeDate,
notAfter: new Date(notAfter) notAfter: new Date(notAfter)
}, },
@@ -538,10 +552,9 @@ export const certificateAuthorityServiceFactory = ({
}); });
// get latest parent CA certificate // get latest parent CA certificate
const [parentCaCert] = await certificateAuthorityCertDAL.find( if (!parentCa.activeCaCertId)
{ caId: parentCa.id }, throw new BadRequestError({ message: "Parent CA does not have a certificate installed" });
{ sort: [["version", "desc"]] } const parentCaCert = await certificateAuthorityCertDAL.findById(parentCa.activeCaCertId);
);
const decryptedParentCaCert = await kmsDecryptor({ const decryptedParentCaCert = await kmsDecryptor({
cipherTextBlob: parentCaCert.encryptedCertificate cipherTextBlob: parentCaCert.encryptedCertificate
@@ -586,7 +599,7 @@ export const certificateAuthorityServiceFactory = ({
const intermediateCert = await x509.X509CertificateGenerator.create({ const intermediateCert = await x509.X509CertificateGenerator.create({
serialNumber, serialNumber,
subject: csrObj.subject, subject: csrObj.subject,
issuer: caCertObj.subject, issuer: parentCaCertObj.subject,
notBefore: notBeforeDate, notBefore: notBeforeDate,
notAfter: new Date(notAfter), notAfter: new Date(notAfter),
signingKey: parentCaPrivateKey, signingKey: parentCaPrivateKey,
@@ -605,7 +618,7 @@ export const certificateAuthorityServiceFactory = ({
ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength, ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength,
true true
), ),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), await x509.AuthorityKeyIdentifierExtension.create(parentCaCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
] ]
}); });
@@ -615,7 +628,7 @@ export const certificateAuthorityServiceFactory = ({
}); });
const { caCert: parentCaCertificate, caCertChain: parentCaCertChain } = await getCaCertChain({ const { caCert: parentCaCertificate, caCertChain: parentCaCertChain } = await getCaCertChain({
caId: parentCa.id, caCertId: parentCa.activeCaCertId,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
projectDAL, projectDAL,
@@ -629,13 +642,12 @@ export const certificateAuthorityServiceFactory = ({
}); });
await certificateAuthorityDAL.transaction(async (tx) => { await certificateAuthorityDAL.transaction(async (tx) => {
const newActiveCaCertVersion = caCert.version + 1; const newCaCert = await certificateAuthorityCertDAL.create(
await certificateAuthorityCertDAL.create(
{ {
caId: ca.id, caId: ca.id,
encryptedCertificate, encryptedCertificate,
encryptedCertificateChain, encryptedCertificateChain,
version: newActiveCaCertVersion, version: caCert.version + 1,
caSecretId: caSecret.id caSecretId: caSecret.id
}, },
tx tx
@@ -644,7 +656,7 @@ export const certificateAuthorityServiceFactory = ({
await certificateAuthorityDAL.updateById( await certificateAuthorityDAL.updateById(
ca.id, ca.id,
{ {
activeCaCertVersion: newActiveCaCertVersion, activeCaCertId: newCaCert.id,
notBefore: notBeforeDate, notBefore: notBeforeDate,
notAfter: new Date(notAfter) notAfter: new Date(notAfter)
}, },
@@ -703,11 +715,11 @@ export const certificateAuthorityServiceFactory = ({
/** /**
* Return current certificate and certificate chain for CA * Return current certificate and certificate chain for CA
* get latest?? ca cert
*/ */
const getCaCert = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertDTO) => { const getCaCert = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertDTO) => {
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" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -723,7 +735,7 @@ export const certificateAuthorityServiceFactory = ({
); );
const { caCert, caCertChain, serialNumber } = await getCaCertChain({ const { caCert, caCertChain, serialNumber } = await getCaCertChain({
caId, caCertId: ca.activeCaCertId,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
projectDAL, projectDAL,
@@ -769,9 +781,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" });
const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (ca.notAfter && new Date() > new Date(ca.notAfter)) { if (ca.notAfter && new Date() > new Date(ca.notAfter)) {
throw new BadRequestError({ message: "CA is expired" }); throw new BadRequestError({ message: "CA is expired" });
@@ -859,7 +871,7 @@ export const certificateAuthorityServiceFactory = ({
}); });
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caId, caCertId: ca.activeCaCertId,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
projectDAL, projectDAL,
@@ -905,8 +917,7 @@ export const certificateAuthorityServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities ProjectPermissionSub.CertificateAuthorities
); );
const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); if (ca.activeCaCertId) throw new BadRequestError({ message: "CA has already imported a certificate" });
if (caCert) throw new BadRequestError({ message: "CA has already imported a certificate" });
const certObj = new x509.X509Certificate(certificate); const certObj = new x509.X509Certificate(certificate);
const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength; const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
@@ -972,7 +983,7 @@ export const certificateAuthorityServiceFactory = ({
} }
await certificateAuthorityCertDAL.transaction(async (tx) => { await certificateAuthorityCertDAL.transaction(async (tx) => {
await certificateAuthorityCertDAL.create( const newCaCert = await certificateAuthorityCertDAL.create(
{ {
caId: ca.id, caId: ca.id,
encryptedCertificate, encryptedCertificate,
@@ -991,7 +1002,8 @@ export const certificateAuthorityServiceFactory = ({
notBefore: new Date(certObj.notBefore), notBefore: new Date(certObj.notBefore),
notAfter: new Date(certObj.notAfter), notAfter: new Date(certObj.notAfter),
serialNumber: certObj.serialNumber, serialNumber: certObj.serialNumber,
parentCaId: parentCa?.id parentCaId: parentCa?.id,
activeCaCertId: newCaCert.id
}, },
tx tx
); );
@@ -1007,6 +1019,7 @@ export const certificateAuthorityServiceFactory = ({
const issueCertFromCa = async ({ const issueCertFromCa = async ({
caId, caId,
certificateTemplateId, certificateTemplateId,
pkiCollectionId,
friendlyName, friendlyName,
commonName, commonName,
altNames, altNames,
@@ -1049,14 +1062,20 @@ export const certificateAuthorityServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
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" });
const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (ca.notAfter && new Date() > new Date(ca.notAfter)) { if (ca.notAfter && new Date() > new Date(ca.notAfter)) {
throw new BadRequestError({ message: "CA is expired" }); throw new BadRequestError({ message: "CA is expired" });
} }
// check PKI collection
if (pkiCollectionId) {
const pkiCollection = await pkiCollectionDAL.findById(pkiCollectionId);
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
if (pkiCollection.projectId !== ca.projectId) throw new BadRequestError({ message: "Invalid PKI collection" });
}
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId, projectId: ca.projectId,
projectDAL, projectDAL,
@@ -1198,6 +1217,7 @@ export const certificateAuthorityServiceFactory = ({
const cert = await certificateDAL.create( const cert = await certificateDAL.create(
{ {
caId: ca.id, caId: ca.id,
caCertId: caCert.id,
status: CertStatus.ACTIVE, status: CertStatus.ACTIVE,
friendlyName: friendlyName || commonName, friendlyName: friendlyName || commonName,
commonName, commonName,
@@ -1217,11 +1237,21 @@ export const certificateAuthorityServiceFactory = ({
tx tx
); );
if (pkiCollectionId) {
await pkiCollectionItemDAL.create(
{
pkiCollectionId,
certId: cert.id
},
tx
);
}
return cert; return cert;
}); });
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caId: ca.id, caCertId: caCert.id,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
projectDAL, projectDAL,
@@ -1246,6 +1276,7 @@ export const certificateAuthorityServiceFactory = ({
caId, caId,
certificateTemplateId, certificateTemplateId,
csr, csr,
pkiCollectionId,
friendlyName, friendlyName,
commonName, commonName,
altNames, altNames,
@@ -1288,14 +1319,21 @@ export const certificateAuthorityServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
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" });
const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (ca.notAfter && new Date() > new Date(ca.notAfter)) { if (ca.notAfter && new Date() > new Date(ca.notAfter)) {
throw new BadRequestError({ message: "CA is expired" }); throw new BadRequestError({ message: "CA is expired" });
} }
// check PKI collection
if (pkiCollectionId) {
const pkiCollection = await pkiCollectionDAL.findById(pkiCollectionId);
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
if (pkiCollection.projectId !== ca.projectId) throw new BadRequestError({ message: "Invalid PKI collection" });
}
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId, projectId: ca.projectId,
projectDAL, projectDAL,
@@ -1432,6 +1470,7 @@ export const certificateAuthorityServiceFactory = ({
const cert = await certificateDAL.create( const cert = await certificateDAL.create(
{ {
caId: ca.id, caId: ca.id,
caCertId: caCert.id,
status: CertStatus.ACTIVE, status: CertStatus.ACTIVE,
friendlyName: friendlyName || csrObj.subject, friendlyName: friendlyName || csrObj.subject,
commonName: cn, commonName: cn,
@@ -1451,11 +1490,21 @@ export const certificateAuthorityServiceFactory = ({
tx tx
); );
if (pkiCollectionId) {
await pkiCollectionItemDAL.create(
{
pkiCollectionId,
certId: cert.id
},
tx
);
}
return cert; return cert;
}); });
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caId: ca.id, caCertId: ca.activeCaCertId,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
projectDAL, projectDAL,
@@ -88,6 +88,7 @@ export type TImportCertToCaDTO = {
export type TIssueCertFromCaDTO = { export type TIssueCertFromCaDTO = {
caId?: string; caId?: string;
certificateTemplateId?: string; certificateTemplateId?: string;
pkiCollectionId?: string;
friendlyName?: string; friendlyName?: string;
commonName: string; commonName: string;
altNames: string; altNames: string;
@@ -100,6 +101,7 @@ export type TSignCertFromCaDTO = {
caId?: string; caId?: string;
csr: string; csr: string;
certificateTemplateId?: string; certificateTemplateId?: string;
pkiCollectionId?: string;
friendlyName?: string; friendlyName?: string;
commonName?: string; commonName?: string;
altNames: string; altNames: string;
@@ -134,9 +136,9 @@ export type TGetCaCertChainsDTO = {
}; };
export type TGetCaCertChainDTO = { export type TGetCaCertChainDTO = {
caId: string; caCertId: string;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findOne">; certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">; kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
}; };
@@ -21,7 +21,7 @@ type TCertificateServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">; certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">; certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findOne">; 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">; projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "findById" | "transaction">;
@@ -180,7 +180,7 @@ export const certificateServiceFactory = ({
const certObj = new x509.X509Certificate(decryptedCert); const certObj = new x509.X509Certificate(decryptedCert);
const { caCert, caCertChain } = await getCaCertChain({ const { caCert, caCertChain } = await getCaCertChain({
caId: ca.id, caCertId: cert.caCertId,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
projectDAL, projectDAL,
@@ -40,6 +40,7 @@ export const pkiCollectionServiceFactory = ({
}: TPkiCollectionServiceFactoryDep) => { }: TPkiCollectionServiceFactoryDep) => {
const createPkiCollection = async ({ const createPkiCollection = async ({
name, name,
description,
projectId, projectId,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
@@ -61,7 +62,8 @@ export const pkiCollectionServiceFactory = ({
const pkiCollection = await pkiCollectionDAL.create({ const pkiCollection = await pkiCollectionDAL.create({
projectId, projectId,
name name,
description
}); });
return pkiCollection; return pkiCollection;
@@ -92,6 +94,7 @@ export const pkiCollectionServiceFactory = ({
const updatePkiCollection = async ({ const updatePkiCollection = async ({
collectionId, collectionId,
name, name,
description,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actor, actor,
@@ -110,7 +113,8 @@ export const pkiCollectionServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections);
pkiCollection = await pkiCollectionDAL.updateById(collectionId, { pkiCollection = await pkiCollectionDAL.updateById(collectionId, {
name name,
description
}); });
return pkiCollection; return pkiCollection;
@@ -2,6 +2,7 @@ import { TProjectPermission } from "@app/lib/types";
export type TCreatePkiCollectionDTO = { export type TCreatePkiCollectionDTO = {
name: string; name: string;
description: string;
} & TProjectPermission; } & TProjectPermission;
export type TGetPkiCollectionByIdDTO = { export type TGetPkiCollectionByIdDTO = {
@@ -11,6 +12,7 @@ export type TGetPkiCollectionByIdDTO = {
export type TUpdatePkiCollectionDTO = { export type TUpdatePkiCollectionDTO = {
collectionId: string; collectionId: string;
name?: string; name?: string;
description?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TDeletePkiCollectionDTO = { export type TDeletePkiCollectionDTO = {
+1
View File
@@ -124,6 +124,7 @@ export const useRenewCa = () => {
}, },
onSuccess: (_, { caId, projectSlug }) => { onSuccess: (_, { caId, projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug })); queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
queryClient.invalidateQueries(caKeys.getCaById(caId));
queryClient.invalidateQueries(caKeys.getCaCert(caId)); queryClient.invalidateQueries(caKeys.getCaCert(caId));
queryClient.invalidateQueries(caKeys.getCaCerts(caId)); queryClient.invalidateQueries(caKeys.getCaCerts(caId));
queryClient.invalidateQueries(caKeys.getCaCsr(caId)); queryClient.invalidateQueries(caKeys.getCaCsr(caId));
+2
View File
@@ -19,6 +19,7 @@ export type TCertificateAuthority = {
notAfter?: string; notAfter?: string;
notBefore?: string; notBefore?: string;
keyAlgorithm: CertKeyAlgorithm; keyAlgorithm: CertKeyAlgorithm;
activeCaCertId?: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}; };
@@ -80,6 +81,7 @@ export type TCreateCertificateDTO = {
projectSlug: string; projectSlug: string;
caId?: string; caId?: string;
certificateTemplateId?: string; certificateTemplateId?: string;
pkiCollectionId?: string;
friendlyName?: string; friendlyName?: string;
commonName: string; commonName: string;
altNames: string; // sans altNames: string; // sans
@@ -1,6 +1,7 @@
export type TPkiCollection = { export type TPkiCollection = {
id: string; id: string;
name: string; name: string;
description: string;
projectId: string; projectId: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
@@ -9,12 +10,14 @@ export type TPkiCollection = {
export type TCreatePkiCollectionDTO = { export type TCreatePkiCollectionDTO = {
projectId: string; projectId: string;
name: string; name: string;
description: string;
}; };
export type TUpdatePkiCollectionTO = { export type TUpdatePkiCollectionTO = {
collectionId: string; collectionId: string;
projectId: string; projectId: string;
name?: string; name?: string;
description?: string;
}; };
export type TDeletePkiCollectionDTO = { export type TDeletePkiCollectionDTO = {
@@ -1,5 +1,6 @@
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { PkiCollectionSection } from "../PkiAlertsTab/components";
import { CertificateTemplatesSection } from "./components/CertificateTemplatesSection"; import { CertificateTemplatesSection } from "./components/CertificateTemplatesSection";
import { CertificatesSection } from "./components"; import { CertificatesSection } from "./components";
@@ -12,6 +13,7 @@ export const CertificatesTab = () => {
animate={{ opacity: 1, translateX: 0 }} animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }} exit={{ opacity: 0, translateX: 30 }}
> >
<PkiCollectionSection />
<CertificateTemplatesSection /> <CertificateTemplatesSection />
<CertificatesSection /> <CertificatesSection />
</motion.div> </motion.div>
@@ -20,7 +20,8 @@ import {
useGetCert, useGetCert,
useGetCertTemplate, useGetCertTemplate,
useListWorkspaceCas, useListWorkspaceCas,
useListWorkspaceCertificateTemplates useListWorkspaceCertificateTemplates,
useListWorkspacePkiCollections
} 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";
@@ -30,6 +31,7 @@ import { CertificateContent } from "./CertificateContent";
const schema = z.object({ const schema = z.object({
certificateTemplateId: z.string().optional(), certificateTemplateId: z.string().optional(),
caId: z.string(), caId: z.string(),
collectionId: z.string().optional(),
friendlyName: z.string(), friendlyName: z.string(),
commonName: z.string().trim().min(1), commonName: z.string().trim().min(1),
altNames: z.string(), altNames: z.string(),
@@ -64,6 +66,10 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
status: CaStatus.ACTIVE status: CaStatus.ACTIVE
}); });
const { data } = useListWorkspacePkiCollections({
workspaceId: currentWorkspace?.id || ""
});
const { data: templatesData } = useListWorkspaceCertificateTemplates({ const { data: templatesData } = useListWorkspaceCertificateTemplates({
workspaceId: currentWorkspace?.id || "" workspaceId: currentWorkspace?.id || ""
}); });
@@ -116,7 +122,14 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
} }
}, [selectedCertTemplate, cert]); }, [selectedCertTemplate, cert]);
const onFormSubmit = async ({ caId, friendlyName, commonName, altNames, ttl }: FormData) => { const onFormSubmit = async ({
caId,
friendlyName,
collectionId,
commonName,
altNames,
ttl
}: FormData) => {
try { try {
if (!currentWorkspace?.slug) return; if (!currentWorkspace?.slug) return;
@@ -124,6 +137,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
caId: !selectedCertTemplate ? caId : undefined, caId: !selectedCertTemplate ? caId : undefined,
certificateTemplateId: selectedCertTemplate ? selectedCertTemplateId : undefined, certificateTemplateId: selectedCertTemplate ? selectedCertTemplateId : undefined,
projectSlug: currentWorkspace.slug, projectSlug: currentWorkspace.slug,
pkiCollectionId: collectionId,
friendlyName, friendlyName,
commonName, commonName,
altNames, altNames,
@@ -230,6 +244,32 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
)} )}
/> />
)} )}
<Controller
control={control}
name="collectionId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Certificate Collection (Optional)"
errorText={error?.message}
isError={Boolean(error)}
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 <Controller
control={control} control={control}
defaultValue="" defaultValue=""
@@ -15,7 +15,8 @@ import {
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({ const schema = z.object({
name: z.string().trim().min(1) name: z.string().trim().min(1),
description: z.string()
}); });
export type FormData = z.infer<typeof schema>; export type FormData = z.infer<typeof schema>;
@@ -49,16 +50,18 @@ export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => {
useEffect(() => { useEffect(() => {
if (pkiCollection) { if (pkiCollection) {
reset({ reset({
name: pkiCollection.name name: pkiCollection.name,
description: pkiCollection.description
}); });
} else { } else {
reset({ reset({
name: "" name: "",
description: ""
}); });
} }
}, [pkiCollection]); }, [pkiCollection]);
const onFormSubmit = async ({ name }: FormData) => { const onFormSubmit = async ({ name, description }: FormData) => {
try { try {
if (!projectId) return; if (!projectId) return;
@@ -67,12 +70,14 @@ export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => {
await updatePkiCollection({ await updatePkiCollection({
collectionId: pkiCollection.id, collectionId: pkiCollection.id,
name, name,
description,
projectId projectId
}); });
} else { } else {
// create // create
const { id: createdId } = await createPkiCollection({ const { id: createdId } = await createPkiCollection({
name, name,
description,
projectId projectId
}); });
@@ -121,6 +126,19 @@ export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => {
</FormControl> </FormControl>
)} )}
/> />
<Controller
control={control}
defaultValue=""
name="description"
render={({ field, fieldState: { error } }) => (
<FormControl label="Description" isError={Boolean(error)} errorText={error?.message}>
<Input
{...field}
placeholder="A collection to house certificates for ACME device"
/>
</FormControl>
)}
/>
<div className="flex items-center"> <div className="flex items-center">
<Button <Button
className="mr-4" className="mr-4"
@@ -48,7 +48,7 @@ export const PkiCollectionSection = () => {
return ( return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between"> <div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Certificate Collection</p> <p className="text-xl font-semibold text-mineshaft-100">Certificate Collections</p>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.PkiCollections} a={ProjectPermissionSub.PkiCollections}
@@ -46,11 +46,12 @@ export const PkiCollectionTable = ({ handlePopUpOpen }: Props) => {
<THead> <THead>
<Tr> <Tr>
<Th>Name</Th> <Th>Name</Th>
<Th>Description</Th>
<Th className="w-5" /> <Th className="w-5" />
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{isLoading && <TableSkeleton columns={2} innerKey="pki-collections" />} {isLoading && <TableSkeleton columns={3} innerKey="pki-collections" />}
{!isLoading && {!isLoading &&
data?.collections.map((pkiCollection) => { data?.collections.map((pkiCollection) => {
return ( return (
@@ -62,6 +63,7 @@ export const PkiCollectionTable = ({ handlePopUpOpen }: Props) => {
} }
> >
<Td>{pkiCollection.name}</Td> <Td>{pkiCollection.name}</Td>
<Td>{pkiCollection.description}</Td>
<Td> <Td>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
@@ -71,10 +71,14 @@ export const PkiCollectionDetailsSection = ({ collectionId, handlePopUpOpen }: P
</div> </div>
</div> </div>
</div> </div>
<div> <div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Name</p> <p className="text-sm font-semibold text-mineshaft-300">Name</p>
<p className="text-sm text-mineshaft-300">{pkiCollection.name}</p> <p className="text-sm text-mineshaft-300">{pkiCollection.name}</p>
</div> </div>
<div>
<p className="text-sm font-semibold text-mineshaft-300">Description</p>
<p className="text-sm text-mineshaft-300">{pkiCollection.description}</p>
</div>
</div> </div>
</div> </div>
) : ( ) : (