Address PR suggestions

This commit is contained in:
Carlos Monastyrski
2025-10-24 03:02:26 -03:00
parent e364eb15db
commit 0ba6b5e86b
21 changed files with 578 additions and 439 deletions
@@ -12,15 +12,15 @@ export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.Certificate, "renewBeforeDays"))) { if (!(await knex.schema.hasColumn(TableName.Certificate, "renewBeforeDays"))) {
await knex.schema.alterTable(TableName.Certificate, (t) => { await knex.schema.alterTable(TableName.Certificate, (t) => {
t.integer("renewBeforeDays").nullable(); t.integer("renewBeforeDays").nullable();
t.uuid("renewedFromId").nullable(); t.uuid("renewedFromCertificateId").nullable();
t.uuid("renewedById").nullable(); t.uuid("renewedByCertificateId").nullable();
t.text("renewalError").nullable(); t.text("renewalError").nullable();
t.string("keyAlgorithm").nullable(); t.string("keyAlgorithm").nullable();
t.string("signatureAlgorithm").nullable(); t.string("signatureAlgorithm").nullable();
t.foreign("renewedFromId").references("id").inTable(TableName.Certificate).onDelete("SET NULL"); t.foreign("renewedFromCertificateId").references("id").inTable(TableName.Certificate).onDelete("SET NULL");
t.foreign("renewedById").references("id").inTable(TableName.Certificate).onDelete("SET NULL"); t.foreign("renewedByCertificateId").references("id").inTable(TableName.Certificate).onDelete("SET NULL");
t.index("renewedFromId"); t.index("renewedFromCertificateId");
t.index("renewedById"); t.index("renewedByCertificateId");
t.index("renewBeforeDays"); t.index("renewBeforeDays");
}); });
} }
@@ -29,14 +29,14 @@ export async function up(knex: Knex): Promise<void> {
export async function down(knex: Knex): Promise<void> { export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.Certificate, "renewBeforeDays")) { if (await knex.schema.hasColumn(TableName.Certificate, "renewBeforeDays")) {
await knex.schema.alterTable(TableName.Certificate, (t) => { await knex.schema.alterTable(TableName.Certificate, (t) => {
t.dropForeign(["renewedFromId"]); t.dropForeign(["renewedFromCertificateId"]);
t.dropForeign(["renewedById"]); t.dropForeign(["renewedByCertificateId"]);
t.dropIndex("renewedFromId"); t.dropIndex("renewedFromCertificateId");
t.dropIndex("renewedById"); t.dropIndex("renewedByCertificateId");
t.dropIndex("renewBeforeDays"); t.dropIndex("renewBeforeDays");
t.dropColumn("renewBeforeDays"); t.dropColumn("renewBeforeDays");
t.dropColumn("renewedFromId"); t.dropColumn("renewedFromCertificateId");
t.dropColumn("renewedById"); t.dropColumn("renewedByCertificateId");
t.dropColumn("renewalError"); t.dropColumn("renewalError");
t.dropColumn("keyAlgorithm"); t.dropColumn("keyAlgorithm");
t.dropColumn("signatureAlgorithm"); t.dropColumn("signatureAlgorithm");
+2 -2
View File
@@ -29,8 +29,8 @@ export const CertificatesSchema = z.object({
pkiSubscriberId: z.string().uuid().nullable().optional(), pkiSubscriberId: z.string().uuid().nullable().optional(),
profileId: z.string().uuid().nullable().optional(), profileId: z.string().uuid().nullable().optional(),
renewBeforeDays: z.number().nullable().optional(), renewBeforeDays: z.number().nullable().optional(),
renewedFromId: z.string().uuid().nullable().optional(), renewedFromCertificateId: z.string().uuid().nullable().optional(),
renewedById: z.string().uuid().nullable().optional(), renewedByCertificateId: z.string().uuid().nullable().optional(),
renewalError: z.string().nullable().optional(), renewalError: z.string().nullable().optional(),
keyAlgorithm: z.string().nullable().optional(), keyAlgorithm: z.string().nullable().optional(),
signatureAlgorithm: z.string().nullable().optional() signatureAlgorithm: z.string().nullable().optional()
@@ -2470,6 +2470,7 @@ interface AutomatedRenewCertificate {
commonName: string; commonName: string;
profileId: string; profileId: string;
renewBeforeDays: string; renewBeforeDays: string;
profileName: string;
}; };
} }
@@ -2480,6 +2481,7 @@ interface AutomatedRenewCertificateFailed {
commonName: string; commonName: string;
profileId: string; profileId: string;
renewBeforeDays: string; renewBeforeDays: string;
profileName: string;
error: string; error: string;
}; };
} }
@@ -2752,6 +2754,7 @@ interface RenewCertificate {
originalCertificateId: string; originalCertificateId: string;
newCertificateId: string; newCertificateId: string;
profileName: string; profileName: string;
commonName: string;
}; };
} }
@@ -4049,6 +4052,7 @@ interface UpdateCertificateRenewalConfigEvent {
metadata: { metadata: {
certificateId: string; certificateId: string;
renewBeforeDays: string; renewBeforeDays: string;
commonName: string;
}; };
} }
@@ -4056,6 +4060,7 @@ interface DisableCertificateRenewalConfigEvent {
type: EventType.DISABLE_CERTIFICATE_RENEWAL_CONFIG; type: EventType.DISABLE_CERTIFICATE_RENEWAL_CONFIG;
metadata: { metadata: {
certificateId: string; certificateId: string;
commonName: string;
}; };
} }
@@ -84,8 +84,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
}) })
) )
.optional(), .optional(),
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(), signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional() keyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
}) })
.refine(validateTtlAndDateFields, { .refine(validateTtlAndDateFields, {
message: message:
@@ -170,8 +170,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
.refine((val) => ms(val) > 0, "TTL must be a positive number"), .refine((val) => ms(val) > 0, "TTL must be a positive number"),
notBefore: validateCaDateField.optional(), notBefore: validateCaDateField.optional(),
notAfter: validateCaDateField.optional(), notAfter: validateCaDateField.optional(),
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(), signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional() keyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
}) })
.refine(validateTtlAndDateFields, { .refine(validateTtlAndDateFields, {
message: message:
@@ -260,8 +260,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
notBefore: validateCaDateField.optional(), notBefore: validateCaDateField.optional(),
notAfter: validateCaDateField.optional(), notAfter: validateCaDateField.optional(),
commonName: validateTemplateRegexField.optional(), commonName: validateTemplateRegexField.optional(),
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(), signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional() keyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
}) })
.refine(validateTtlAndDateFields, { .refine(validateTtlAndDateFields, {
message: message:
@@ -385,7 +385,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
metadata: { metadata: {
originalCertificateId: req.params.certificateId, originalCertificateId: req.params.certificateId,
newCertificateId: data.certificateId, newCertificateId: data.certificateId,
profileName: data.profileName profileName: data.profileName,
commonName: data.commonName
} }
} }
}); });
@@ -409,10 +410,10 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
body: z body: z
.object({ .object({
renewBeforeDays: z.number().int().min(1).max(30).optional(), renewBeforeDays: z.number().int().min(1).max(30).optional(),
disableAutoRenewal: z.boolean().optional() enableAutoRenewal: z.boolean().optional()
}) })
.refine((data) => !(data.renewBeforeDays !== undefined && data.disableAutoRenewal === true), { .refine((data) => !(data.renewBeforeDays !== undefined && data.enableAutoRenewal === false), {
message: "Cannot specify both renewBeforeDays and disableAutoRenewal" message: "Cannot specify both renewBeforeDays and enableAutoRenewal=false"
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -423,7 +424,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
if (req.body.disableAutoRenewal === true) { if (req.body.enableAutoRenewal === false) {
const data = await server.services.certificateV3.disableRenewalConfig({ const data = await server.services.certificateV3.disableRenewalConfig({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
@@ -438,7 +439,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
event: { event: {
type: EventType.DISABLE_CERTIFICATE_RENEWAL_CONFIG, type: EventType.DISABLE_CERTIFICATE_RENEWAL_CONFIG,
metadata: { metadata: {
certificateId: req.params.certificateId certificateId: req.params.certificateId,
commonName: data.commonName
} }
} }
}); });
@@ -465,7 +467,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
type: EventType.UPDATE_CERTIFICATE_RENEWAL_CONFIG, type: EventType.UPDATE_CERTIFICATE_RENEWAL_CONFIG,
metadata: { metadata: {
certificateId: req.params.certificateId, certificateId: req.params.certificateId,
renewBeforeDays: req.body.renewBeforeDays.toString() renewBeforeDays: req.body.renewBeforeDays.toString(),
commonName: data.commonName
} }
} }
}); });
@@ -2,12 +2,14 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { Knex } from "knex";
import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionCertificateProfileActions,
ProjectPermissionPkiTemplateActions, ProjectPermissionPkiTemplateActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
@@ -1181,7 +1183,8 @@ export const internalCertificateAuthorityServiceFactory = ({
signatureAlgorithm, signatureAlgorithm,
keyAlgorithm, keyAlgorithm,
isFromProfile, isFromProfile,
internal = false internal = false,
tx
}: TIssueCertFromCaDTO) => { }: TIssueCertFromCaDTO) => {
let ca: TCertificateAuthorityWithAssociatedCa | undefined; let ca: TCertificateAuthorityWithAssociatedCa | undefined;
let certificateTemplate: TCertificateTemplates | undefined; let certificateTemplate: TCertificateTemplates | undefined;
@@ -1221,10 +1224,17 @@ export const internalCertificateAuthorityServiceFactory = ({
actionProjectType: ActionProjectType.CertificateManager actionProjectType: ActionProjectType.CertificateManager
}); });
ForbiddenError.from(permission).throwUnlessCan( if (isFromProfile) {
ProjectPermissionCertificateActions.Create, ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSub.Certificates ProjectPermissionCertificateProfileActions.IssueCert,
); ProjectPermissionSub.CertificateProfiles
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Create,
ProjectPermissionSub.Certificates
);
}
} }
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
@@ -1476,7 +1486,7 @@ export const internalCertificateAuthorityServiceFactory = ({
plainText: Buffer.from(certificateChainPem) plainText: Buffer.from(certificateChainPem)
}); });
await certificateDAL.transaction(async (tx) => { const executeIssueCertOperations = async (transaction: Knex) => {
const cert = await certificateDAL.create( const cert = await certificateDAL.create(
{ {
caId: (ca as TCertificateAuthorities).id, caId: (ca as TCertificateAuthorities).id,
@@ -1495,7 +1505,7 @@ export const internalCertificateAuthorityServiceFactory = ({
keyAlgorithm: effectiveKeyAlgorithm, keyAlgorithm: effectiveKeyAlgorithm,
signatureAlgorithm: signatureAlgorithm || ca!.internalCa!.keyAlgorithm signatureAlgorithm: signatureAlgorithm || ca!.internalCa!.keyAlgorithm
}, },
tx transaction
); );
await certificateBodyDAL.create( await certificateBodyDAL.create(
@@ -1504,7 +1514,7 @@ export const internalCertificateAuthorityServiceFactory = ({
encryptedCertificate, encryptedCertificate,
encryptedCertificateChain encryptedCertificateChain
}, },
tx transaction
); );
await certificateSecretDAL.create( await certificateSecretDAL.create(
@@ -1512,7 +1522,7 @@ export const internalCertificateAuthorityServiceFactory = ({
certId: cert.id, certId: cert.id,
encryptedPrivateKey encryptedPrivateKey
}, },
tx transaction
); );
if (collectionId) { if (collectionId) {
@@ -1521,12 +1531,18 @@ export const internalCertificateAuthorityServiceFactory = ({
pkiCollectionId: collectionId, pkiCollectionId: collectionId,
certId: cert.id certId: cert.id
}, },
tx transaction
); );
} }
return cert; return cert;
}); };
if (tx) {
await executeIssueCertOperations(tx);
} else {
await certificateDAL.transaction(executeIssueCertOperations);
}
return { return {
certificate: leafCert.toString("pem"), certificate: leafCert.toString("pem"),
@@ -1598,10 +1614,17 @@ export const internalCertificateAuthorityServiceFactory = ({
actionProjectType: ActionProjectType.CertificateManager actionProjectType: ActionProjectType.CertificateManager
}); });
ForbiddenError.from(permission).throwUnlessCan( if (dto.isFromProfile && dto.profileId) {
ProjectPermissionCertificateActions.Create, ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSub.Certificates ProjectPermissionCertificateProfileActions.IssueCert,
); ProjectPermissionSub.CertificateProfiles
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Create,
ProjectPermissionSub.Certificates
);
}
} }
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
@@ -1,3 +1,4 @@
import { Knex } from "knex";
import { z } from "zod"; import { z } from "zod";
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";
@@ -139,7 +140,9 @@ export type TIssueCertFromCaDTO = {
signatureAlgorithm?: CertSignatureAlgorithm; signatureAlgorithm?: CertSignatureAlgorithm;
keyAlgorithm?: CertKeyAlgorithm; keyAlgorithm?: CertKeyAlgorithm;
isFromProfile?: boolean; isFromProfile?: boolean;
profileId?: string;
internal?: boolean; internal?: boolean;
tx?: Knex;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TSignCertFromCaDTO = export type TSignCertFromCaDTO =
@@ -160,6 +163,7 @@ export type TSignCertFromCaDTO =
signatureAlgorithm?: string; signatureAlgorithm?: string;
keyAlgorithm?: string; keyAlgorithm?: string;
isFromProfile?: boolean; isFromProfile?: boolean;
profileId?: string;
} }
| ({ | ({
isInternal: false; isInternal: false;
@@ -178,6 +182,7 @@ export type TSignCertFromCaDTO =
signatureAlgorithm?: string; signatureAlgorithm?: string;
keyAlgorithm?: string; keyAlgorithm?: string;
isFromProfile?: boolean; isFromProfile?: boolean;
profileId?: string;
} & Omit<TProjectPermission, "projectId">); } & Omit<TProjectPermission, "projectId">);
export type TGetCaCertificateTemplatesDTO = { export type TGetCaCertificateTemplatesDTO = {
@@ -187,24 +187,6 @@ export enum CertificateRenewalErrorType {
UNKNOWN_ERROR = "UNKNOWN_ERROR" UNKNOWN_ERROR = "UNKNOWN_ERROR"
} }
export const CERTIFICATE_RENEWAL_ERROR_MESSAGES = {
[CertificateRenewalErrorType.TEMPLATE_VALIDATION_FAILED]:
"Auto-renewal failed: certificate template policy has changed and this certificate no longer meets the requirements",
[CertificateRenewalErrorType.CA_NOT_FOUND]:
"Auto-renewal failed: Certificate Authority for this certificate is no longer available",
[CertificateRenewalErrorType.CA_INACTIVE]: "Auto-renewal failed: Certificate Authority is currently inactive",
[CertificateRenewalErrorType.CERTIFICATE_OUTLIVES_CA]:
"Auto-renewal failed: certificate would outlive the Certificate Authority",
[CertificateRenewalErrorType.TTL_TOO_SHORT]:
"Auto-renewal failed: certificate validity period is too short for the renewal threshold",
[CertificateRenewalErrorType.NOT_ELIGIBLE]: "Auto-renewal failed: certificate is not eligible for automatic renewal",
[CertificateRenewalErrorType.VALIDITY_EXCEEDS_MAXIMUM]:
"Auto-renewal failed: certificate validity period exceeds the maximum allowed by the profile template",
[CertificateRenewalErrorType.NOT_ALLOWED_BY_TEMPLATE]:
"Auto-renewal failed: certificate settings are no longer allowed by the profile template",
[CertificateRenewalErrorType.UNKNOWN_ERROR]: "Auto-renewal failed: an unexpected error occurred"
} as const;
export const CERTIFICATE_RENEWAL_CONFIG = { export const CERTIFICATE_RENEWAL_CONFIG = {
MIN_RENEW_BEFORE_DAYS: 1, MIN_RENEW_BEFORE_DAYS: 1,
MAX_RENEW_BEFORE_DAYS: 30, MAX_RENEW_BEFORE_DAYS: 30,
@@ -1,12 +1,8 @@
import RE2 from "re2"; import RE2 from "re2";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { CertExtendedKeyUsage, CertKeyUsage } from "../certificate/certificate-types"; import { CertExtendedKeyUsage, CertKeyUsage } from "../certificate/certificate-types";
import { import {
CertExtendedKeyUsageType, CertExtendedKeyUsageType,
CERTIFICATE_RENEWAL_ERROR_MESSAGES,
CertificateRenewalErrorType,
CertKeyUsageType, CertKeyUsageType,
mapExtendedKeyUsageToLegacy, mapExtendedKeyUsageToLegacy,
mapKeyUsageToLegacy, mapKeyUsageToLegacy,
@@ -200,74 +196,3 @@ export const convertExtendedKeyUsageArrayToLegacy = (
): CertExtendedKeyUsage[] | undefined => { ): CertExtendedKeyUsage[] | undefined => {
return usages?.map(convertToLegacyExtendedKeyUsage); return usages?.map(convertToLegacyExtendedKeyUsage);
}; };
export const categorizeCertificateRenewalError = (error: unknown): string => {
if (!error) {
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.UNKNOWN_ERROR];
}
const errorMessage = error instanceof Error ? error.message : String(error);
if (error instanceof NotFoundError) {
if (errorMessage.includes("Certificate Authority")) {
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.CA_NOT_FOUND];
}
if (errorMessage.includes("Certificate template")) {
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.TEMPLATE_VALIDATION_FAILED];
}
}
if (error instanceof BadRequestError) {
if (errorMessage.includes("Certificate Authority is") && errorMessage.includes("must be ACTIVE")) {
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.CA_INACTIVE];
}
if (errorMessage.includes("would expire") && errorMessage.includes("after its issuing CA")) {
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.CERTIFICATE_OUTLIVES_CA];
}
if (errorMessage.includes("TTL") && errorMessage.includes("must be greater than renewal threshold")) {
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.TTL_TOO_SHORT];
}
if (errorMessage.includes("not eligible for renewal")) {
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.NOT_ELIGIBLE];
}
if (errorMessage.includes("Requested validity period exceeds maximum allowed duration")) {
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.VALIDITY_EXCEEDS_MAXIMUM];
}
if (errorMessage.includes("not allowed by template policy")) {
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.NOT_ALLOWED_BY_TEMPLATE];
}
}
if (error instanceof ForbiddenRequestError) {
if (errorMessage.includes("Template validation failed")) {
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.TEMPLATE_VALIDATION_FAILED];
}
}
if (errorMessage.includes("Template validation failed")) {
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.TEMPLATE_VALIDATION_FAILED];
}
if (errorMessage.includes("Certificate Authority") && errorMessage.includes("not found")) {
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.CA_NOT_FOUND];
}
if (errorMessage.includes("Certificate Authority is") && errorMessage.includes("must be ACTIVE")) {
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.CA_INACTIVE];
}
if (errorMessage.includes("would expire") && errorMessage.includes("after its issuing CA")) {
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.CERTIFICATE_OUTLIVES_CA];
}
if (errorMessage.includes("TTL") && errorMessage.includes("must be greater than renewal threshold")) {
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.TTL_TOO_SHORT];
}
if (errorMessage.includes("not eligible for renewal")) {
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.NOT_ELIGIBLE];
}
if (errorMessage.includes("Requested validity period exceeds maximum allowed duration")) {
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.VALIDITY_EXCEEDS_MAXIMUM];
}
if (errorMessage.includes("not allowed by template policy")) {
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.NOT_ALLOWED_BY_TEMPLATE];
}
return `${CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.UNKNOWN_ERROR]}: ${errorMessage}`;
};
@@ -6,7 +6,6 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { ActorType } from "../auth/auth-type"; import { ActorType } from "../auth/auth-type";
import { TCertificateDALFactory } from "../certificate/certificate-dal"; import { TCertificateDALFactory } from "../certificate/certificate-dal";
import { CERTIFICATE_RENEWAL_CONFIG } from "../certificate-common/certificate-constants"; import { CERTIFICATE_RENEWAL_CONFIG } from "../certificate-common/certificate-constants";
import { categorizeCertificateRenewalError } from "../certificate-common/certificate-utils";
import { TCertificateV3ServiceFactory } from "./certificate-v3-service"; import { TCertificateV3ServiceFactory } from "./certificate-v3-service";
type TCertificateV3QueueServiceFactoryDep = { type TCertificateV3QueueServiceFactoryDep = {
@@ -70,9 +69,6 @@ export const certificateV3QueueServiceFactory = ({
internal: true internal: true
}); });
await certificateDAL.updateById(certificate.id, {
renewalError: null
});
totalCertificatesRenewed += 1; totalCertificatesRenewed += 1;
await auditLogService.createAuditLog({ await auditLogService.createAuditLog({
@@ -87,42 +83,32 @@ export const certificateV3QueueServiceFactory = ({
certificateId: certificate.id, certificateId: certificate.id,
commonName: certificate.commonName || "", commonName: certificate.commonName || "",
profileId: certificate.profileId!, profileId: certificate.profileId!,
renewBeforeDays: certificate.renewBeforeDays?.toString() || "" renewBeforeDays: certificate.renewBeforeDays?.toString() || "",
profileName: certificate.profileName || ""
} }
} }
}); });
} catch (error) { } catch (error) {
const categorizedError: string = categorizeCertificateRenewalError(error); const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(error, `Failed to renew certificate ${certificate.id}: ${errorMessage}`);
try { await auditLogService.createAuditLog({
await certificateDAL.updateById(certificate.id, { projectId: certificate.projectId,
renewalError: categorizedError actor: {
}); type: ActorType.PLATFORM,
} catch (updateError) { metadata: {}
logger.error(updateError, `Failed to update renewal error for certificate ${certificate.id}`); },
} event: {
type: EventType.AUTOMATED_RENEW_CERTIFICATE_FAILED,
try { metadata: {
await auditLogService.createAuditLog({ certificateId: certificate.id,
projectId: certificate.projectId, commonName: certificate.commonName || "",
actor: { profileId: certificate.profileId || "",
type: ActorType.PLATFORM, renewBeforeDays: certificate.renewBeforeDays?.toString() || "",
metadata: {} profileName: certificate.profileName || "",
}, error: errorMessage
event: {
type: EventType.AUTOMATED_RENEW_CERTIFICATE_FAILED,
metadata: {
certificateId: certificate.id,
commonName: certificate.commonName || "",
profileId: certificate.profileId || "",
renewBeforeDays: certificate.renewBeforeDays?.toString() || "",
error: categorizedError
}
} }
}); }
} catch (auditError) { });
logger.error(auditError, `Failed to create audit log for failed certificate renewal ${certificate.id}`);
}
} }
} }
@@ -29,10 +29,14 @@ import { certificateV3ServiceFactory, TCertificateV3ServiceFactory } from "./cer
describe("CertificateV3Service", () => { describe("CertificateV3Service", () => {
let service: TCertificateV3ServiceFactory; let service: TCertificateV3ServiceFactory;
const mockCertificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById"> = { const mockCertificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction"> = {
findOne: vi.fn(), findOne: vi.fn(),
findById: vi.fn(), findById: vi.fn(),
updateById: vi.fn() updateById: vi.fn(),
transaction: vi.fn().mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
return callback(mockTx);
})
}; };
const mockCertificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa"> = { const mockCertificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa"> = {
@@ -78,7 +82,7 @@ describe("CertificateV3Service", () => {
beforeEach(() => { beforeEach(() => {
// Reset all mocks before each test // Reset all mocks before each test
vi.clearAllMocks(); vi.resetAllMocks();
// Mock ForbiddenError.from static method // Mock ForbiddenError.from static method
vi.spyOn(ForbiddenError, "from").mockReturnValue({ vi.spyOn(ForbiddenError, "from").mockReturnValue({
@@ -1473,7 +1477,7 @@ describe("CertificateV3Service", () => {
notBefore: new Date("2024-01-01"), notBefore: new Date("2024-01-01"),
notAfter: new Date("2024-02-01"), // 31 days notAfter: new Date("2024-02-01"), // 31 days
revokedAt: null, revokedAt: null,
renewedById: null, renewedByCertificateId: null,
profileId: "profile-123", profileId: "profile-123",
renewBeforeDays: 7, renewBeforeDays: 7,
caId: "ca-123", caId: "ca-123",
@@ -1487,7 +1491,7 @@ describe("CertificateV3Service", () => {
certificateTemplateId: "template-123", certificateTemplateId: "template-123",
revocationReason: null, revocationReason: null,
caCertId: null, caCertId: null,
renewedFromId: null, renewedFromCertificateId: null,
renewalError: null, renewalError: null,
keyAlgorithm: "RSA_2048", keyAlgorithm: "RSA_2048",
signatureAlgorithm: "RSA-SHA256" signatureAlgorithm: "RSA-SHA256"
@@ -1570,8 +1574,6 @@ describe("CertificateV3Service", () => {
}; };
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks();
// Mock current date to be within renewal window // Mock current date to be within renewal window
vi.useFakeTimers(); vi.useFakeTimers();
vi.setSystemTime(new Date("2024-01-26")); // 6 days before cert expires, within renewal window vi.setSystemTime(new Date("2024-01-26")); // 6 days before cert expires, within renewal window
@@ -1582,6 +1584,7 @@ describe("CertificateV3Service", () => {
}); });
it("should successfully renew eligible certificate", async () => { it("should successfully renew eligible certificate", async () => {
// Mock the initial findById call
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert); vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA); vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
@@ -1604,6 +1607,13 @@ describe("CertificateV3Service", () => {
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(newCert); vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(newCert);
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(newCert); vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(newCert);
// Mock the transaction to return the expected structure
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
const result = await callback(mockTx);
return result;
});
const result = await service.renewCertificate({ const result = await service.renewCertificate({
certificateId: "cert-123", certificateId: "cert-123",
...mockActor ...mockActor
@@ -1611,15 +1621,23 @@ describe("CertificateV3Service", () => {
expect(result).toHaveProperty("certificate", "renewed-cert"); expect(result).toHaveProperty("certificate", "renewed-cert");
expect(result).toHaveProperty("certificateId", "cert-456"); expect(result).toHaveProperty("certificateId", "cert-456");
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-456", { expect(mockCertificateDAL.updateById).toHaveBeenCalledWith(
profileId: "profile-123", "cert-456",
renewBeforeDays: 14, {
renewedFromId: "cert-123" profileId: "profile-123",
}); renewBeforeDays: 14,
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", { renewedFromCertificateId: "cert-123"
renewedById: "cert-456", },
renewalError: null {}
}); );
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith(
"cert-123",
{
renewedByCertificateId: "cert-456",
renewalError: null
},
{}
);
}); });
it("should validate certificate against current template during renewal", async () => { it("should validate certificate against current template during renewal", async () => {
@@ -1633,6 +1651,15 @@ describe("CertificateV3Service", () => {
warnings: [] warnings: []
}); });
// Mock updateById to handle the renewal error logging
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockOriginalCert);
// Set up transaction mock to properly handle errors
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
return callback(mockTx);
});
await expect( await expect(
service.renewCertificate({ service.renewCertificate({
certificateId: "cert-123", certificateId: "cert-123",
@@ -1645,9 +1672,7 @@ describe("CertificateV3Service", () => {
certificateId: "cert-123", certificateId: "cert-123",
...mockActor ...mockActor
}) })
).rejects.toThrow( ).rejects.toThrow("Certificate renewal failed. Errors: Subject alternative name not allowed");
"Certificate renewal failed because requested validity period exceeds maximum allowed duration by the profile template: Subject alternative name not allowed"
);
// Should store template validation error // Should store template validation error
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", { expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", {
@@ -1659,6 +1684,12 @@ describe("CertificateV3Service", () => {
const certWithoutProfile = { ...mockOriginalCert, profileId: null }; const certWithoutProfile = { ...mockOriginalCert, profileId: null };
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(certWithoutProfile); vi.mocked(mockCertificateDAL.findById).mockResolvedValue(certWithoutProfile);
// Set up transaction mock to properly handle errors
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
return callback(mockTx);
});
await expect( await expect(
service.renewCertificate({ service.renewCertificate({
certificateId: "cert-123", certificateId: "cert-123",
@@ -1675,11 +1706,20 @@ describe("CertificateV3Service", () => {
}); });
it("should reject renewal if certificate is already renewed", async () => { it("should reject renewal if certificate is already renewed", async () => {
const alreadyRenewedCert = { ...mockOriginalCert, renewedById: "cert-456" }; const alreadyRenewedCert = { ...mockOriginalCert, renewedByCertificateId: "cert-456" };
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(alreadyRenewedCert); vi.mocked(mockCertificateDAL.findById).mockResolvedValue(alreadyRenewedCert);
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA); vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
// Mock updateById to handle the renewal error logging
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(alreadyRenewedCert);
// Set up transaction mock to properly handle errors
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
return callback(mockTx);
});
await expect( await expect(
service.renewCertificate({ service.renewCertificate({
certificateId: "cert-123", certificateId: "cert-123",
@@ -1704,6 +1744,15 @@ describe("CertificateV3Service", () => {
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA); vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
// Mock updateById to handle the renewal error logging
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(expiredCert);
// Set up transaction mock to properly handle errors
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
return callback(mockTx);
});
await expect( await expect(
service.renewCertificate({ service.renewCertificate({
certificateId: "cert-123", certificateId: "cert-123",
@@ -1728,6 +1777,15 @@ describe("CertificateV3Service", () => {
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA); vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
// Mock updateById to handle the renewal error logging
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(revokedCert);
// Set up transaction mock to properly handle errors
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
return callback(mockTx);
});
await expect( await expect(
service.renewCertificate({ service.renewCertificate({
certificateId: "cert-123", certificateId: "cert-123",
@@ -1749,6 +1807,15 @@ describe("CertificateV3Service", () => {
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(inactiveCA); vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(inactiveCA);
// Mock updateById to handle the renewal error logging
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockOriginalCert);
// Set up transaction mock to properly handle errors
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
return callback(mockTx);
});
await expect( await expect(
service.renewCertificate({ service.renewCertificate({
certificateId: "cert-123", certificateId: "cert-123",
@@ -1776,6 +1843,15 @@ describe("CertificateV3Service", () => {
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(shortLivedCA); vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(shortLivedCA);
// Mock updateById to handle the renewal error logging
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockOriginalCert);
// Set up transaction mock to properly handle errors
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
return callback(mockTx);
});
await expect( await expect(
service.renewCertificate({ service.renewCertificate({
certificateId: "cert-123", certificateId: "cert-123",
@@ -1816,6 +1892,12 @@ describe("CertificateV3Service", () => {
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(newCert); vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(newCert);
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(newCert); vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(newCert);
// Set up transaction mock to properly handle the renewal process
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
return callback(mockTx);
});
const result = await service.renewCertificate({ const result = await service.renewCertificate({
certificateId: "cert-123", certificateId: "cert-123",
...mockActor ...mockActor
@@ -1830,12 +1912,13 @@ describe("CertificateV3Service", () => {
const mockCert = { const mockCert = {
id: "cert-123", id: "cert-123",
profileId: "profile-123", profileId: "profile-123",
renewedById: null, renewedByCertificateId: null,
notBefore: new Date("2026-01-01"), notBefore: new Date("2026-01-01"),
notAfter: new Date("2026-02-01"), notAfter: new Date("2026-02-01"),
projectId: "project-123", projectId: "project-123",
status: CertStatus.ACTIVE, status: CertStatus.ACTIVE,
revokedAt: null revokedAt: null,
commonName: ""
}; };
const mockProfile = { const mockProfile = {
@@ -1859,7 +1942,8 @@ describe("CertificateV3Service", () => {
expect(result).toEqual({ expect(result).toEqual({
projectId: "project-123", projectId: "project-123",
renewBeforeDays: 7 renewBeforeDays: 7,
commonName: ""
}); });
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", { expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", {
@@ -1871,7 +1955,7 @@ describe("CertificateV3Service", () => {
const mockCert = { const mockCert = {
id: "cert-123", id: "cert-123",
profileId: null, profileId: null,
renewedById: null, renewedByCertificateId: null,
projectId: "project-123" projectId: "project-123"
}; };
@@ -1904,7 +1988,7 @@ describe("CertificateV3Service", () => {
const mockCert = { const mockCert = {
id: "cert-123", id: "cert-123",
profileId: "profile-123", profileId: "profile-123",
renewedById: "cert-456", renewedByCertificateId: "cert-456",
projectId: "project-123", projectId: "project-123",
status: CertStatus.ACTIVE, status: CertStatus.ACTIVE,
revokedAt: null, revokedAt: null,
@@ -1948,7 +2032,7 @@ describe("CertificateV3Service", () => {
const mockCert = { const mockCert = {
id: "cert-123", id: "cert-123",
profileId: "profile-123", profileId: "profile-123",
renewedById: null, renewedByCertificateId: null,
notBefore: new Date("2026-01-01"), notBefore: new Date("2026-01-01"),
notAfter: new Date("2026-01-08"), notAfter: new Date("2026-01-08"),
projectId: "project-123", projectId: "project-123",
@@ -1994,7 +2078,8 @@ describe("CertificateV3Service", () => {
const mockCert = { const mockCert = {
id: "cert-123", id: "cert-123",
profileId: "profile-123", profileId: "profile-123",
projectId: "project-123" projectId: "project-123",
commonName: ""
}; };
const mockProfile = { const mockProfile = {
@@ -2016,7 +2101,8 @@ describe("CertificateV3Service", () => {
}); });
expect(result).toEqual({ expect(result).toEqual({
projectId: "project-123" projectId: "project-123",
commonName: ""
}); });
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", { expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", {
@@ -16,6 +16,7 @@ import {
CertExtendedKeyUsage, CertExtendedKeyUsage,
CertificateOrderStatus, CertificateOrderStatus,
CertKeyAlgorithm, CertKeyAlgorithm,
CertKeyType,
CertKeyUsage, CertKeyUsage,
CertSignatureAlgorithm, CertSignatureAlgorithm,
CertStatus CertStatus
@@ -56,7 +57,7 @@ import {
} from "./certificate-v3-types"; } from "./certificate-v3-types";
type TCertificateV3ServiceFactoryDep = { type TCertificateV3ServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById">; certificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">; certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">;
certificateTemplateV2Service: Pick< certificateTemplateV2Service: Pick<
@@ -114,7 +115,7 @@ const validateRenewalEligibility = (
notBefore: Date; notBefore: Date;
notAfter: Date; notAfter: Date;
revokedAt?: Date | null; revokedAt?: Date | null;
renewedById?: string | null; renewedByCertificateId?: string | null;
profileId?: string | null; profileId?: string | null;
caId?: string | null; caId?: string | null;
pkiSubscriberId?: string | null; pkiSubscriberId?: string | null;
@@ -153,7 +154,7 @@ const validateRenewalEligibility = (
errors.push(`Certificate Authority is ${ca.status}, must be ${CaStatus.ACTIVE}`); errors.push(`Certificate Authority is ${ca.status}, must be ${CaStatus.ACTIVE}`);
} }
if (certificate.renewedById) { if (certificate.renewedByCertificateId) {
errors.push("Certificate has already been renewed"); errors.push("Certificate has already been renewed");
} }
@@ -212,11 +213,11 @@ const validateAlgorithmCompatibility = (
const keyType = parts[parts.length - 1]; const keyType = parts[parts.length - 1];
if (caKeyAlgorithm.startsWith("RSA")) { if (caKeyAlgorithm.startsWith("RSA")) {
return keyType === "RSA"; return keyType === CertKeyType.RSA;
} }
if (caKeyAlgorithm.startsWith("EC")) { if (caKeyAlgorithm.startsWith("EC")) {
return keyType === "ECDSA"; return keyType === CertKeyType.ECDSA;
} }
return false; return false;
@@ -338,7 +339,8 @@ export const certificateV3ServiceFactory = ({
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
templateId: profile.certificateTemplateId templateId: profile.certificateTemplateId,
internal: true
}); });
if (!template) { if (!template) {
throw new NotFoundError({ message: "Certificate template not found for this profile" }); throw new NotFoundError({ message: "Certificate template not found for this profile" });
@@ -362,10 +364,6 @@ export const certificateV3ServiceFactory = ({
validateCaSupport(ca, "direct certificate issuance"); validateCaSupport(ca, "direct certificate issuance");
if (!actorAuthMethod) {
throw new BadRequestError({ message: "Authentication method is required for certificate issuance" });
}
validateAlgorithmCompatibility(ca, template); validateAlgorithmCompatibility(ca, template);
const effectiveSignatureAlgorithm = certificateRequest.signatureAlgorithm as CertSignatureAlgorithm | undefined; const effectiveSignatureAlgorithm = certificateRequest.signatureAlgorithm as CertSignatureAlgorithm | undefined;
@@ -433,7 +431,8 @@ export const certificateV3ServiceFactory = ({
serialNumber, serialNumber,
certificateId: cert.id, certificateId: cert.id,
projectId: profile.projectId, projectId: profile.projectId,
profileName: profile.slug profileName: profile.slug,
commonName: cert.commonName || ""
}; };
}; };
@@ -468,16 +467,13 @@ export const certificateV3ServiceFactory = ({
validateCaSupport(ca, "CSR signing"); validateCaSupport(ca, "CSR signing");
if (!actorAuthMethod) {
throw new BadRequestError({ message: "Authentication method is required for certificate signing" });
}
const template = await certificateTemplateV2Service.getTemplateV2ById({ const template = await certificateTemplateV2Service.getTemplateV2ById({
actor, actor,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
templateId: profile.certificateTemplateId templateId: profile.certificateTemplateId,
internal: true
}); });
if (!template) { if (!template) {
@@ -541,7 +537,8 @@ export const certificateV3ServiceFactory = ({
serialNumber, serialNumber,
certificateId: cert.id, certificateId: cert.id,
projectId: profile.projectId, projectId: profile.projectId,
profileName: profile.slug profileName: profile.slug,
commonName: cert.commonName || ""
}; };
}; };
@@ -645,178 +642,224 @@ export const certificateV3ServiceFactory = ({
actorOrgId, actorOrgId,
internal = false internal = false
}: TRenewCertificateDTO & { internal?: boolean }): Promise<TCertificateFromProfileResponse> => { }: TRenewCertificateDTO & { internal?: boolean }): Promise<TCertificateFromProfileResponse> => {
const originalCert = await certificateDAL.findById(certificateId); const renewalResult = await certificateDAL.transaction(async (tx) => {
if (!originalCert) { const originalCert = await certificateDAL.findById(certificateId, tx);
throw new NotFoundError({ message: "Certificate not found" }); if (!originalCert) {
} throw new NotFoundError({ message: "Certificate not found" });
if (!originalCert.profileId) {
throw new ForbiddenRequestError({
message: "Only certificates issued from a profile can be renewed"
});
}
const originalSignatureAlgorithm = originalCert.signatureAlgorithm as CertSignatureAlgorithm;
const originalKeyAlgorithm = originalCert.keyAlgorithm as CertKeyAlgorithm;
if (!originalSignatureAlgorithm || !originalKeyAlgorithm) {
throw new BadRequestError({
message:
"Original certificate does not have algorithm information stored. Cannot renew certificate issued before algorithm tracking was implemented."
});
}
const profile = await certificateProfileDAL.findByIdWithConfigs(originalCert.profileId);
if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" });
}
if (profile.enrollmentType !== "api") {
throw new ForbiddenRequestError({
message: "Certificate is not eligible for renewal: EST certificates cannot be renewed through this endpoint"
});
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
if (!ca) {
throw new NotFoundError({ message: "Certificate Authority not found" });
}
const eligibilityCheck = validateRenewalEligibility(originalCert, ca);
if (!eligibilityCheck.isEligible) {
throw new BadRequestError({
message: `Certificate is not eligible for renewal: ${eligibilityCheck.errors.join(", ")}`
});
}
if (!internal) {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: profile.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.IssueCert,
ProjectPermissionSub.CertificateProfiles
);
}
validateCaSupport(ca, "direct certificate issuance");
const template = await certificateTemplateV2Service.getTemplateV2ById({
actor,
actorId,
actorAuthMethod,
actorOrgId,
templateId: profile.certificateTemplateId,
internal
});
if (!template) {
throw new NotFoundError({ message: "Certificate template not found for this profile" });
}
const originalTtlInDays = Math.ceil(
(new Date(originalCert.notAfter).getTime() - new Date(originalCert.notBefore).getTime()) / (1000 * 60 * 60 * 24)
);
const ttl = `${originalTtlInDays}d`;
const certificateRequest = {
commonName: originalCert.commonName || undefined,
keyUsages: convertKeyUsageArrayFromLegacy(parseKeyUsages(originalCert.keyUsages)),
extendedKeyUsages: convertExtendedKeyUsageArrayFromLegacy(parseExtendedKeyUsages(originalCert.extendedKeyUsages)),
subjectAlternativeNames: originalCert.altNames
? originalCert.altNames.split(",").map((san) => {
const trimmed = san.trim();
const isIp =
trimmed.length <= 45 &&
(new RE2("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$").test(trimmed) ||
new RE2("^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$").test(trimmed));
return {
type: isIp ? CertSubjectAlternativeNameType.IP_ADDRESS : CertSubjectAlternativeNameType.DNS_NAME,
value: trimmed
};
})
: [],
validity: {
ttl
} }
};
const validationResult = await certificateTemplateV2Service.validateCertificateRequest( if (!originalCert.profileId) {
profile.certificateTemplateId, throw new ForbiddenRequestError({
certificateRequest message: "Only certificates issued from a profile can be renewed"
); });
}
if (!validationResult.isValid) { const originalSignatureAlgorithm = originalCert.signatureAlgorithm as CertSignatureAlgorithm;
await certificateDAL.updateById(originalCert.id, { const originalKeyAlgorithm = originalCert.keyAlgorithm as CertKeyAlgorithm;
renewalError: `Template validation failed: ${validationResult.errors.join(", ")}`
});
throw new BadRequestError({ if (!originalSignatureAlgorithm || !originalKeyAlgorithm) {
message: `Certificate renewal failed because requested validity period exceeds maximum allowed duration by the profile template: ${validationResult.errors.join(", ")}` throw new BadRequestError({
}); message:
} "Original certificate does not have algorithm information stored. Cannot renew certificate issued before algorithm tracking was implemented."
});
}
validateAlgorithmCompatibility(ca, template); const profile = await certificateProfileDAL.findByIdWithConfigs(originalCert.profileId);
const notBefore = new Date(); if (!profile) {
const notAfter = new Date(Date.now() + parseTtlToDays(ttl) * 24 * 60 * 60 * 1000); throw new NotFoundError({ message: "Certificate profile not found" });
}
const { certificate, certificateChain, issuingCaCertificate, serialNumber } = if (profile.enrollmentType !== EnrollmentType.API) {
await internalCaService.issueCertFromCa({ throw new ForbiddenRequestError({
caId: ca.id, message: "Certificate is not eligible for renewal: EST certificates cannot be renewed through this endpoint"
friendlyName: originalCert.friendlyName || originalCert.commonName || "Renewed Certificate", });
commonName: originalCert.commonName || "", }
altNames: originalCert.altNames || "",
ttl, if (!internal) {
notBefore: normalizeDateForApi(notBefore), const { permission } = await permissionService.getProjectPermission({
notAfter: normalizeDateForApi(notAfter), actor,
keyUsages: parseKeyUsages(originalCert.keyUsages), actorId,
extendedKeyUsages: parseExtendedKeyUsages(originalCert.extendedKeyUsages), projectId: profile.projectId,
signatureAlgorithm: originalSignatureAlgorithm, actorAuthMethod,
keyAlgorithm: originalKeyAlgorithm, actorOrgId,
isFromProfile: true, actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.IssueCert,
ProjectPermissionSub.CertificateProfiles
);
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
if (!ca) {
throw new NotFoundError({ message: "Certificate Authority not found" });
}
const eligibilityCheck = validateRenewalEligibility(originalCert, ca);
if (!eligibilityCheck.isEligible) {
await certificateDAL.updateById(originalCert.id, {
renewalError: `Certificate is not eligible for renewal: ${eligibilityCheck.errors.join(", ")}`
});
throw new BadRequestError({
message: `Certificate is not eligible for renewal: ${eligibilityCheck.errors.join(", ")}`
});
}
validateCaSupport(ca, "direct certificate issuance");
const template = await certificateTemplateV2Service.getTemplateV2ById({
actor, actor,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
templateId: profile.certificateTemplateId,
internal internal
}); });
const newCert = await certificateDAL.findOne({ serialNumber, caId: ca.id }); if (!template) {
if (!newCert) { throw new NotFoundError({ message: "Certificate template not found for this profile" });
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" }); }
}
const certificateTtlInDays = parseTtlToDays(ttl); const originalTtlInDays = Math.ceil(
const finalRenewBeforeDays = calculateRenewalThreshold(profile.apiConfig?.renewBeforeDays, certificateTtlInDays); (new Date(originalCert.notAfter).getTime() - new Date(originalCert.notBefore).getTime()) / (1000 * 60 * 60 * 24)
);
const ttl = `${originalTtlInDays}d`;
await certificateDAL.updateById(newCert.id, { const certificateRequest = {
profileId: originalCert.profileId, commonName: originalCert.commonName || undefined,
renewBeforeDays: finalRenewBeforeDays, keyUsages: convertKeyUsageArrayFromLegacy(parseKeyUsages(originalCert.keyUsages)),
renewedFromId: originalCert.id extendedKeyUsages: convertExtendedKeyUsageArrayFromLegacy(
parseExtendedKeyUsages(originalCert.extendedKeyUsages)
),
subjectAlternativeNames: originalCert.altNames
? originalCert.altNames.split(",").map((san) => {
const trimmed = san.trim();
const isIpv4 = new RE2("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$").test(trimmed);
const isIpv6 = new RE2("^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$").test(trimmed);
if (isIpv4 || isIpv6) {
return {
type: CertSubjectAlternativeNameType.IP_ADDRESS,
value: trimmed
};
}
if (new RE2("^[^@]+@[^@]+\\.[^@]+$").test(trimmed)) {
return {
type: CertSubjectAlternativeNameType.EMAIL,
value: trimmed
};
}
if (new RE2("^[a-zA-Z][a-zA-Z0-9+.-]*:").test(trimmed)) {
return {
type: CertSubjectAlternativeNameType.URI,
value: trimmed
};
}
return {
type: CertSubjectAlternativeNameType.DNS_NAME,
value: trimmed
};
})
: [],
validity: {
ttl
},
signatureAlgorithm: originalCert.signatureAlgorithm || undefined,
keyAlgorithm: originalCert.keyAlgorithm || undefined
};
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
profile.certificateTemplateId,
certificateRequest
);
if (!validationResult.isValid) {
await certificateDAL.updateById(originalCert.id, {
renewalError: `Template validation failed: ${validationResult.errors.join(", ")}`
});
throw new BadRequestError({
message: `Certificate renewal failed. Errors: ${validationResult.errors.join(", ")}`
});
}
validateAlgorithmCompatibility(ca, template);
const notBefore = new Date();
const notAfter = new Date(Date.now() + parseTtlToDays(ttl) * 24 * 60 * 60 * 1000);
const certificateTtlInDays = parseTtlToDays(ttl);
const finalRenewBeforeDays = calculateRenewalThreshold(profile.apiConfig?.renewBeforeDays, certificateTtlInDays);
const { certificate, certificateChain, issuingCaCertificate, serialNumber } =
await internalCaService.issueCertFromCa({
caId: ca.id,
friendlyName: originalCert.friendlyName || originalCert.commonName || "Renewed Certificate",
commonName: originalCert.commonName || "",
altNames: originalCert.altNames || "",
ttl,
notBefore: normalizeDateForApi(notBefore),
notAfter: normalizeDateForApi(notAfter),
keyUsages: parseKeyUsages(originalCert.keyUsages),
extendedKeyUsages: parseExtendedKeyUsages(originalCert.extendedKeyUsages),
signatureAlgorithm: originalSignatureAlgorithm,
keyAlgorithm: originalKeyAlgorithm,
isFromProfile: true,
actor,
actorId,
actorAuthMethod,
actorOrgId,
internal: true,
tx
});
const newCert = await certificateDAL.findOne({ serialNumber, caId: ca.id }, tx);
if (!newCert) {
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
}
await certificateDAL.updateById(
newCert.id,
{
profileId: originalCert.profileId,
renewBeforeDays: finalRenewBeforeDays,
renewedFromCertificateId: originalCert.id
},
tx
);
await certificateDAL.updateById(
originalCert.id,
{
renewedByCertificateId: newCert.id,
renewalError: null
},
tx
);
return {
certificate,
certificateChain,
issuingCaCertificate,
serialNumber,
newCert,
originalCert,
profile
};
}); });
await certificateDAL.updateById(originalCert.id, {
renewedById: newCert.id,
renewalError: null
});
const certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer);
const certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer);
return { return {
certificate: certificateString, certificate: renewalResult.certificate,
issuingCaCertificate: extractCertificateFromBuffer(issuingCaCertificate as unknown as Buffer), issuingCaCertificate: renewalResult.issuingCaCertificate,
certificateChain: certificateChainString, certificateChain: renewalResult.certificateChain,
serialNumber, serialNumber: renewalResult.serialNumber,
certificateId: newCert.id, certificateId: renewalResult.newCert.id,
projectId: profile.projectId, projectId: renewalResult.profile.projectId,
profileName: profile.slug profileName: renewalResult.profile.slug,
commonName: renewalResult.originalCert.commonName || ""
}; };
}; };
@@ -858,7 +901,7 @@ export const certificateV3ServiceFactory = ({
throw new NotFoundError({ message: "Certificate profile not found" }); throw new NotFoundError({ message: "Certificate profile not found" });
} }
if (profile.enrollmentType !== "api") { if (profile.enrollmentType !== EnrollmentType.API) {
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Certificate is not eligible for auto-renewal: EST certificates cannot be auto-renewed" message: "Certificate is not eligible for auto-renewal: EST certificates cannot be auto-renewed"
}); });
@@ -883,7 +926,7 @@ export const certificateV3ServiceFactory = ({
}); });
} }
if (certificate.renewedById) { if (certificate.renewedByCertificateId) {
throw new BadRequestError({ throw new BadRequestError({
message: "Certificate is not eligible for auto-renewal: certificate has already been renewed" message: "Certificate is not eligible for auto-renewal: certificate has already been renewed"
}); });
@@ -911,7 +954,8 @@ export const certificateV3ServiceFactory = ({
return { return {
projectId: certificate.projectId, projectId: certificate.projectId,
renewBeforeDays renewBeforeDays,
commonName: certificate.commonName || ""
}; };
}; };
@@ -952,7 +996,7 @@ export const certificateV3ServiceFactory = ({
throw new NotFoundError({ message: "Certificate profile not found" }); throw new NotFoundError({ message: "Certificate profile not found" });
} }
if (profile.enrollmentType !== "api") { if (profile.enrollmentType !== EnrollmentType.API) {
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Certificate is not eligible for auto-renewal: EST certificates cannot be auto-renewed" message: "Certificate is not eligible for auto-renewal: EST certificates cannot be auto-renewed"
}); });
@@ -963,7 +1007,8 @@ export const certificateV3ServiceFactory = ({
}); });
return { return {
projectId: certificate.projectId projectId: certificate.projectId,
commonName: certificate.commonName || ""
}; };
}; };
@@ -68,6 +68,7 @@ export type TCertificateFromProfileResponse = {
certificateId: string; certificateId: string;
projectId: string; projectId: string;
profileName: string; profileName: string;
commonName: string;
}; };
export type TCertificateOrderResponse = { export type TCertificateOrderResponse = {
@@ -114,8 +115,10 @@ export type TDisableRenewalConfigDTO = {
export type TRenewalConfigResponse = { export type TRenewalConfigResponse = {
projectId: string; projectId: string;
renewBeforeDays: number; renewBeforeDays: number;
commonName: string;
}; };
export type TDisableRenewalResponse = { export type TDisableRenewalResponse = {
projectId: string; projectId: string;
commonName: string;
}; };
@@ -1,7 +1,7 @@
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TCertificates } from "@app/db/schemas"; import { TableName, TCertificates } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
import { CertStatus } from "./certificate-types"; import { CertStatus } from "./certificate-types";
@@ -120,32 +120,33 @@ export const certificateDALFactory = (db: TDbClient) => {
}: { }: {
limit: number; limit: number;
offset: number; offset: number;
}): Promise<TCertificates[]> => { }): Promise<(TCertificates & { profileName?: string })[]> => {
try { try {
const now = new Date(); const now = new Date();
const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999); const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
const certs = (await db const certs = (await db
.replicaNode()(TableName.Certificate) .replicaNode()(TableName.Certificate)
.select(`${TableName.Certificate}.*`) .select(selectAllTableCols(TableName.Certificate))
.select(db.ref("slug").withSchema(TableName.PkiCertificateProfile).as("profileName"))
.leftJoin(
TableName.PkiCertificateProfile,
`${TableName.Certificate}.profileId`,
`${TableName.PkiCertificateProfile}.id`
)
.where(`${TableName.Certificate}.status`, CertStatus.ACTIVE) .where(`${TableName.Certificate}.status`, CertStatus.ACTIVE)
.whereNull(`${TableName.Certificate}.renewedById`) .whereNull(`${TableName.Certificate}.renewedByCertificateId`)
.whereNull(`${TableName.Certificate}.renewalError`) .whereNull(`${TableName.Certificate}.renewalError`)
.whereNull(`${TableName.Certificate}.revokedAt`) .whereNull(`${TableName.Certificate}.revokedAt`)
.whereNotNull(`${TableName.Certificate}.profileId`) .whereNotNull(`${TableName.Certificate}.profileId`)
.whereNotNull(`${TableName.Certificate}.notAfter`) .whereNotNull(`${TableName.Certificate}.notAfter`)
.where(`${TableName.Certificate}.notAfter`, ">", now) .where(`${TableName.Certificate}.notAfter`, ">", now)
.where((queryBuilder) => { .whereNotNull(`${TableName.Certificate}.renewBeforeDays`)
void queryBuilder.where((subQuery) => { .where(`${TableName.Certificate}.renewBeforeDays`, ">", 0)
void subQuery .whereRaw(
.whereNotNull(`${TableName.Certificate}.renewBeforeDays`) `"${TableName.Certificate}"."notAfter" - INTERVAL '1 day' * "${TableName.Certificate}"."renewBeforeDays" <= ?`,
.where(`${TableName.Certificate}.renewBeforeDays`, ">", 0) [endOfDay]
.whereRaw( )
`"${TableName.Certificate}"."notAfter" - INTERVAL '1 day' * "${TableName.Certificate}"."renewBeforeDays" <= ?`,
[endOfDay]
);
});
})
.limit(limit) .limit(limit)
.offset(offset) .offset(offset)
.orderBy(`${TableName.Certificate}.notAfter`, "asc")) as TCertificates[]; .orderBy(`${TableName.Certificate}.notAfter`, "asc")) as TCertificates[];
@@ -21,6 +21,11 @@ export enum CertKeyAlgorithm {
ECDSA_P521 = "EC_secp521r1" ECDSA_P521 = "EC_secp521r1"
} }
export enum CertKeyType {
RSA = "RSA",
ECDSA = "ECDSA"
}
export enum CertSignatureAlgorithm { export enum CertSignatureAlgorithm {
RSA_SHA256 = "RSA-SHA256", RSA_SHA256 = "RSA-SHA256",
RSA_SHA384 = "RSA-SHA384", RSA_SHA384 = "RSA-SHA384",
@@ -944,7 +944,7 @@ export const projectServiceFactory = ({
...(friendlyName && { friendlyName }), ...(friendlyName && { friendlyName }),
...(commonName && { commonName }) ...(commonName && { commonName })
}, },
{ offset, limit, sort: [["updatedAt", "desc"]] } { offset, limit, sort: [["notAfter", "desc"]] }
); );
const count = await certificateDAL.countCertificatesInProject({ const count = await certificateDAL.countCertificatesInProject({
@@ -117,10 +117,10 @@ export const useUpdateRenewalConfig = () => {
object, object,
TUpdateRenewalConfigDTO TUpdateRenewalConfigDTO
>({ >({
mutationFn: async ({ certificateId, renewBeforeDays, disableAutoRenewal }) => { mutationFn: async ({ certificateId, renewBeforeDays, enableAutoRenewal }) => {
const { data } = await apiRequest.patch<{ message: string; renewBeforeDays?: number }>( const { data } = await apiRequest.patch<{ message: string; renewBeforeDays?: number }>(
`/api/v3/certificates/${certificateId}/config`, `/api/v3/certificates/${certificateId}/config`,
{ renewBeforeDays, disableAutoRenewal } { renewBeforeDays, enableAutoRenewal }
); );
return data; return data;
}, },
+3 -3
View File
@@ -16,8 +16,8 @@ export type TCertificate = {
extendedKeyUsages: CertExtendedKeyUsage[]; extendedKeyUsages: CertExtendedKeyUsage[];
renewBeforeDays?: number; renewBeforeDays?: number;
renewedBy?: string; renewedBy?: string;
renewedFromId?: string; renewedFromCertificateId?: string;
renewedById?: string; renewedByCertificateId?: string;
renewalError?: string; renewalError?: string;
}; };
@@ -67,6 +67,6 @@ export type TRenewCertificateResponse = {
export type TUpdateRenewalConfigDTO = { export type TUpdateRenewalConfigDTO = {
certificateId: string; certificateId: string;
renewBeforeDays?: number; renewBeforeDays?: number;
disableAutoRenewal?: boolean; enableAutoRenewal?: boolean;
projectSlug: string; projectSlug: string;
}; };
@@ -1,4 +1,4 @@
import { useEffect } from "react"; import { useEffect, useMemo } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
@@ -7,6 +7,7 @@ import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { useProject } from "@app/context"; import { useProject } from "@app/context";
import { useUpdateRenewalConfig } from "@app/hooks/api"; import { useUpdateRenewalConfig } from "@app/hooks/api";
import { useGetCertificateProfileById } from "@app/hooks/api/certificateProfiles";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const DEFAULT_RENEWAL_BEFORE_DAYS = 20; const DEFAULT_RENEWAL_BEFORE_DAYS = 20;
@@ -64,7 +65,8 @@ const RenewalConfigForm = ({
}) => ( }) => (
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<FormControl <FormControl
label="Renewal Days Before Expiration" label="Auto-renew days before expiry"
isError={Boolean(errors.renewBeforeDays)}
errorText={errors.renewBeforeDays?.message} errorText={errors.renewBeforeDays?.message}
className="mb-6" className="mb-6"
> >
@@ -111,10 +113,24 @@ export const CertificateManageRenewalModal = ({ popUp, handlePopUpToggle }: Prop
ttlDays?: number; ttlDays?: number;
notAfter: string; notAfter: string;
renewalError?: string; renewalError?: string;
renewedFromId?: string; renewedFromCertificateId?: string;
renewedById?: string; renewedByCertificateId?: string;
}; };
const { data: profileData } = useGetCertificateProfileById({
profileId: certificateData?.profileId || ""
});
const defaultRenewalDays = useMemo(() => {
if (certificateData?.renewBeforeDays) {
return certificateData.renewBeforeDays;
}
if (profileData?.apiConfig?.renewBeforeDays) {
return profileData.apiConfig.renewBeforeDays;
}
return DEFAULT_RENEWAL_BEFORE_DAYS;
}, [certificateData?.renewBeforeDays, profileData?.apiConfig?.renewBeforeDays]);
const isAutoRenewalEnabled = Boolean( const isAutoRenewalEnabled = Boolean(
certificateData?.renewBeforeDays && certificateData.renewBeforeDays > 0 certificateData?.renewBeforeDays && certificateData.renewBeforeDays > 0
); );
@@ -134,17 +150,17 @@ export const CertificateManageRenewalModal = ({ popUp, handlePopUpToggle }: Prop
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
renewBeforeDays: DEFAULT_RENEWAL_BEFORE_DAYS renewBeforeDays: defaultRenewalDays
} }
}); });
useEffect(() => { useEffect(() => {
if (popUp.manageRenewal.isOpen) { if (popUp.manageRenewal.isOpen) {
reset({ reset({
renewBeforeDays: certificateData?.renewBeforeDays || DEFAULT_RENEWAL_BEFORE_DAYS renewBeforeDays: defaultRenewalDays
}); });
} }
}, [popUp.manageRenewal.isOpen, certificateData?.renewBeforeDays, reset]); }, [popUp.manageRenewal.isOpen, defaultRenewalDays, reset]);
const onUpdateRenewal = async (data: FormData) => { const onUpdateRenewal = async (data: FormData) => {
try { try {
@@ -31,7 +31,7 @@ export const CertificateRenewalDisableModal = ({ popUp, handlePopUpToggle }: Pro
await updateRenewalConfig({ await updateRenewalConfig({
certificateId: certificateData.certificateId, certificateId: certificateData.certificateId,
projectSlug: currentProject.slug, projectSlug: currentProject.slug,
disableAutoRenewal: true enableAutoRenewal: false
}); });
createNotification({ createNotification({
@@ -36,7 +36,8 @@ import {
import { import {
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionSub, ProjectPermissionSub,
useProject useProject,
useSubscription
} from "@app/context"; } from "@app/context";
import { useListWorkspaceCertificates, useUpdateRenewalConfig } from "@app/hooks/api"; import { useListWorkspaceCertificates, useUpdateRenewalConfig } from "@app/hooks/api";
import { caSupportsCapability } from "@app/hooks/api/ca/constants"; import { caSupportsCapability } from "@app/hooks/api/ca/constants";
@@ -56,8 +57,8 @@ const isExpiringWithinOneDay = (notAfter: string): boolean => {
}; };
const getAutoRenewalInfo = (certificate: TCertificate) => { const getAutoRenewalInfo = (certificate: TCertificate) => {
if (certificate.renewedById) { if (certificate.renewedByCertificateId) {
return { text: "Renewed", variant: "success" as const }; return { text: "Renewed", variant: "instance" as const };
} }
const isRevoked = certificate.status === CertStatus.REVOKED; const isRevoked = certificate.status === CertStatus.REVOKED;
@@ -65,8 +66,36 @@ const getAutoRenewalInfo = (certificate: TCertificate) => {
const hasNoProfile = !certificate.profileId; const hasNoProfile = !certificate.profileId;
const isExpiringWithinDay = isExpiringWithinOneDay(certificate.notAfter); const isExpiringWithinDay = isExpiringWithinOneDay(certificate.notAfter);
if (isRevoked || isExpired || hasNoProfile || isExpiringWithinDay) { if (isRevoked) {
return null; return {
text: "Not Available",
variant: "instance" as const,
tooltip: "Auto-renewal is not available for revoked certificates"
};
}
if (isExpired) {
return {
text: "Not Available",
variant: "instance" as const,
tooltip: "Auto-renewal is not available for expired certificates"
};
}
if (hasNoProfile) {
return {
text: "Not Available",
variant: "instance" as const,
tooltip: "Auto-renewal requires a certificate profile"
};
}
if (isExpiringWithinDay) {
return {
text: "Not Available",
variant: "instance" as const,
tooltip: "Auto-renewal is not available for certificates expiring within 24 hours"
};
} }
if (certificate.renewalError) { if (certificate.renewalError) {
@@ -127,8 +156,8 @@ type Props = {
ttlDays?: number; ttlDays?: number;
notAfter?: string; notAfter?: string;
renewalError?: string; renewalError?: string;
renewedFromId?: string; renewedFromCertificateId?: string;
renewedById?: string; renewedByCertificateId?: string;
} }
) => void; ) => void;
}; };
@@ -138,6 +167,7 @@ const PER_PAGE_INIT = 25;
export const CertificatesTable = ({ handlePopUpOpen }: Props) => { export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(PER_PAGE_INIT); const [perPage, setPerPage] = useState(PER_PAGE_INIT);
const { subscription } = useSubscription();
const { currentProject } = useProject(); const { currentProject } = useProject();
const { data, isPending } = useListWorkspaceCertificates({ const { data, isPending } = useListWorkspaceCertificates({
@@ -147,6 +177,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
}); });
const { mutateAsync: updateRenewalConfig } = useUpdateRenewalConfig(); const { mutateAsync: updateRenewalConfig } = useUpdateRenewalConfig();
const isLegacyTemplatesEnabled = subscription.pkiLegacyTemplates;
const { data: caData } = useListCasByProjectId(currentProject?.id ?? ""); const { data: caData } = useListCasByProjectId(currentProject?.id ?? "");
@@ -173,7 +204,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
await updateRenewalConfig({ await updateRenewalConfig({
certificateId, certificateId,
projectSlug: currentProject.slug, projectSlug: currentProject.slug,
disableAutoRenewal: true enableAutoRenewal: false
}); });
createNotification({ createNotification({
@@ -198,7 +229,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
<Th>Status</Th> <Th>Status</Th>
<Th>Not Before</Th> <Th>Not Before</Th>
<Th>Not After</Th> <Th>Not After</Th>
<Th>Auto Renewal</Th> <Th>Renewal Status</Th>
<Th /> <Th />
</Tr> </Tr>
</THead> </THead>
@@ -286,32 +317,34 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
<ProjectPermissionCan {isLegacyTemplatesEnabled && (
I={ProjectPermissionCertificateActions.Read} <ProjectPermissionCan
a={ProjectPermissionSub.Certificates} I={ProjectPermissionCertificateActions.Read}
> a={ProjectPermissionSub.Certificates}
{(isAllowed) => ( >
<DropdownMenuItem {(isAllowed) => (
className={twMerge( <DropdownMenuItem
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50" className={twMerge(
)} !isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
onClick={async () => )}
handlePopUpOpen("certificate", { onClick={async () =>
serialNumber: certificate.serialNumber handlePopUpOpen("certificate", {
}) serialNumber: certificate.serialNumber
} })
disabled={!isAllowed} }
icon={<FontAwesomeIcon icon={faEye} />} disabled={!isAllowed}
> icon={<FontAwesomeIcon icon={faEye} />}
View Details >
</DropdownMenuItem> View Details
)} </DropdownMenuItem>
</ProjectPermissionCan> )}
</ProjectPermissionCan>
)}
{/* Manage auto renewal option - not shown for failed renewals */} {/* Manage auto renewal option - not shown for failed renewals */}
{(() => { {(() => {
const canManageRenewal = const canManageRenewal =
certificate.profileId && certificate.profileId &&
!certificate.renewedById && !certificate.renewedByCertificateId &&
!isRevoked && !isRevoked &&
!isExpired && !isExpired &&
!hasFailed && !hasFailed &&
@@ -353,8 +386,9 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
ttlDays, ttlDays,
notAfter: certificate.notAfter, notAfter: certificate.notAfter,
renewalError: certificate.renewalError, renewalError: certificate.renewalError,
renewedFromId: certificate.renewedFromId, renewedFromCertificateId:
renewedById: certificate.renewedById certificate.renewedFromCertificateId,
renewedByCertificateId: certificate.renewedByCertificateId
}); });
}} }}
disabled={!isAllowed} disabled={!isAllowed}
@@ -373,7 +407,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
{(() => { {(() => {
const canDisableRenewal = const canDisableRenewal =
certificate.profileId && certificate.profileId &&
!certificate.renewedById && !certificate.renewedByCertificateId &&
!isRevoked && !isRevoked &&
!isExpired && !isExpired &&
!isExpiringWithinDay && !isExpiringWithinDay &&
@@ -411,7 +445,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
{(() => { {(() => {
const canRenew = const canRenew =
certificate.profileId && certificate.profileId &&
!certificate.renewedById && !certificate.renewedByCertificateId &&
!isRevoked && !isRevoked &&
!isExpired; !isExpired;
@@ -11,6 +11,26 @@ import {
mapTemplateSignatureAlgorithmToApi mapTemplateSignatureAlgorithmToApi
} from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants"; } from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants";
const convertTemplateTtlToCertificateTtl = (templateTtl: string): string => {
const match = templateTtl.match(/^(\d+)([dmyh])$/);
if (!match) return templateTtl;
const [, value, unit] = match;
const numValue = parseInt(value, 10);
switch (unit) {
case "m":
return `${numValue * 30}d`;
case "y":
return `${numValue * 365}d`;
case "d":
case "h":
return templateTtl;
default:
return templateTtl;
}
};
export type TemplateConstraints = { export type TemplateConstraints = {
allowedKeyUsages: string[]; allowedKeyUsages: string[];
allowedExtendedKeyUsages: string[]; allowedExtendedKeyUsages: string[];
@@ -118,7 +138,7 @@ export const useCertificateTemplate = (
// Set TTL if available // Set TTL if available
if (templateData.validity?.max) { if (templateData.validity?.max) {
setValue("ttl", templateData.validity.max); setValue("ttl", convertTemplateTtlToCertificateTtl(templateData.validity.max));
} }
// Handle SAN types // Handle SAN types