PKI: add support for auto-renewal option on API enrollment type

This commit is contained in:
Carlos Monastyrski
2025-10-23 00:38:24 -03:00
parent 7224ff9ece
commit 9d09990143
34 changed files with 2739 additions and 94 deletions
@@ -0,0 +1,53 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.PkiApiEnrollmentConfig, "autoRenewDays")) {
await knex.schema.alterTable(TableName.PkiApiEnrollmentConfig, (t) => {
t.dropColumn("autoRenewDays");
t.integer("renewBeforeDays");
});
}
if (!(await knex.schema.hasColumn(TableName.Certificate, "renewBeforeDays"))) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.integer("renewBeforeDays").nullable();
t.uuid("renewedFromId").nullable();
t.uuid("renewedById").nullable();
t.text("renewalError").nullable();
t.string("keyAlgorithm").nullable();
t.string("signatureAlgorithm").nullable();
t.foreign("renewedFromId").references("id").inTable(TableName.Certificate).onDelete("SET NULL");
t.foreign("renewedById").references("id").inTable(TableName.Certificate).onDelete("SET NULL");
t.index("renewedFromId");
t.index("renewedById");
t.index("renewBeforeDays");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.Certificate, "renewBeforeDays")) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.dropForeign(["renewedFromId"]);
t.dropForeign(["renewedById"]);
t.dropIndex("renewedFromId");
t.dropIndex("renewedById");
t.dropIndex("renewBeforeDays");
t.dropColumn("renewBeforeDays");
t.dropColumn("renewedFromId");
t.dropColumn("renewedById");
t.dropColumn("renewalError");
t.dropColumn("keyAlgorithm");
t.dropColumn("signatureAlgorithm");
});
}
if (await knex.schema.hasColumn(TableName.PkiApiEnrollmentConfig, "renewBeforeDays")) {
await knex.schema.alterTable(TableName.PkiApiEnrollmentConfig, (t) => {
t.dropColumn("renewBeforeDays");
t.integer("autoRenewDays");
});
}
}
+7 -1
View File
@@ -27,7 +27,13 @@ export const CertificatesSchema = z.object({
extendedKeyUsages: z.string().array().nullable().optional(),
projectId: z.string(),
pkiSubscriberId: z.string().uuid().nullable().optional(),
profileId: z.string().uuid().nullable().optional()
profileId: z.string().uuid().nullable().optional(),
renewBeforeDays: z.number().nullable().optional(),
renewedFromId: z.string().uuid().nullable().optional(),
renewedById: z.string().uuid().nullable().optional(),
renewalError: z.string().nullable().optional(),
keyAlgorithm: z.string().nullable().optional(),
signatureAlgorithm: z.string().nullable().optional()
});
export type TCertificates = z.infer<typeof CertificatesSchema>;
@@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models";
export const PkiApiEnrollmentConfigsSchema = z.object({
id: z.string().uuid(),
autoRenew: z.boolean().default(false).nullable().optional(),
autoRenewDays: z.number().nullable().optional(),
renewBeforeDays: z.number().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
@@ -337,6 +337,8 @@ export enum EventType {
ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert",
SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert",
AUTOMATED_RENEW_SUBSCRIBER_CERT = "automated-renew-subscriber-cert",
AUTOMATED_RENEW_CERTIFICATE = "automated-renew-certificate",
AUTOMATED_RENEW_CERTIFICATE_FAILED = "automated-renew-certificate-failed",
LIST_PKI_SUBSCRIBER_CERTS = "list-pki-subscriber-certs",
GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE = "get-subscriber-active-cert-bundle",
CREATE_KMS = "create-kms",
@@ -364,6 +366,9 @@ export enum EventType {
ISSUE_CERTIFICATE_FROM_PROFILE = "issue-certificate-from-profile",
SIGN_CERTIFICATE_FROM_PROFILE = "sign-certificate-from-profile",
ORDER_CERTIFICATE_FROM_PROFILE = "order-certificate-from-profile",
RENEW_CERTIFICATE = "renew-certificate",
UPDATE_CERTIFICATE_RENEWAL_CONFIG = "update-certificate-renewal-config",
DISABLE_CERTIFICATE_RENEWAL_CONFIG = "disable-certificate-renewal-config",
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
@@ -2437,6 +2442,27 @@ interface AutomatedRenewPkiSubscriberCert {
};
}
interface AutomatedRenewCertificate {
type: EventType.AUTOMATED_RENEW_CERTIFICATE;
metadata: {
certificateId: string;
commonName: string;
profileId: string;
renewBeforeDays: string;
};
}
interface AutomatedRenewCertificateFailed {
type: EventType.AUTOMATED_RENEW_CERTIFICATE_FAILED;
metadata: {
certificateId: string;
commonName: string;
profileId: string;
renewBeforeDays: string;
error: string;
};
}
interface SignPkiSubscriberCert {
type: EventType.SIGN_PKI_SUBSCRIBER_CERT;
metadata: {
@@ -2699,6 +2725,15 @@ interface OrderCertificateFromProfile {
};
}
interface RenewCertificate {
type: EventType.RENEW_CERTIFICATE;
metadata: {
originalCertificateId: string;
newCertificateId: string;
profileName: string;
};
}
interface AttemptCreateSlackIntegration {
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION;
metadata: {
@@ -3963,6 +3998,21 @@ interface PamResourceDeleteEvent {
};
}
interface UpdateCertificateRenewalConfigEvent {
type: EventType.UPDATE_CERTIFICATE_RENEWAL_CONFIG;
metadata: {
certificateId: string;
renewBeforeDays: string;
};
}
interface DisableCertificateRenewalConfigEvent {
type: EventType.DISABLE_CERTIFICATE_RENEWAL_CONFIG;
metadata: {
certificateId: string;
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@@ -4168,6 +4218,7 @@ export type Event =
| IssueCertificateFromProfile
| SignCertificateFromProfile
| OrderCertificateFromProfile
| RenewCertificate
| GetAzureAdCsTemplatesEvent
| AttemptCreateSlackIntegration
| AttemptReinstallSlackIntegration
@@ -4323,4 +4374,8 @@ export type Event =
| PamResourceGetEvent
| PamResourceCreateEvent
| PamResourceUpdateEvent
| PamResourceDeleteEvent;
| PamResourceDeleteEvent
| UpdateCertificateRenewalConfigEvent
| DisableCertificateRenewalConfigEvent
| AutomatedRenewCertificate
| AutomatedRenewCertificateFailed;
+8 -2
View File
@@ -77,7 +77,8 @@ export enum QueueName {
DailyReminders = "daily-reminders",
SecretReminderMigration = "secret-reminder-migration",
UserNotification = "user-notification",
HealthAlert = "health-alert"
HealthAlert = "health-alert",
CertificateV3AutoRenewal = "certificate-v3-auto-renewal"
}
export enum QueueJobs {
@@ -126,7 +127,8 @@ export enum QueueJobs {
DailyReminders = "daily-reminders",
SecretReminderMigration = "secret-reminder-migration",
UserNotification = "user-notification-job",
HealthAlert = "health-alert"
HealthAlert = "health-alert",
CertificateV3DailyAutoRenewal = "certificate-v3-daily-auto-renewal"
}
export type TQueueJobTypes = {
@@ -357,6 +359,10 @@ export type TQueueJobTypes = {
name: QueueJobs.HealthAlert;
payload: undefined;
};
[QueueName.CertificateV3AutoRenewal]: {
name: QueueJobs.CertificateV3DailyAutoRenewal;
payload: undefined;
};
};
const SECRET_SCANNING_JOBS = [
+12
View File
@@ -175,6 +175,7 @@ import { certificateTemplateEstConfigDALFactory } from "@app/services/certificat
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { certificateTemplateV2DALFactory } from "@app/services/certificate-template-v2/certificate-template-v2-dal";
import { certificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
import { certificateV3QueueServiceFactory } from "@app/services/certificate-v3/certificate-v3-queue";
import { certificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service";
import { cmekServiceFactory } from "@app/services/cmek/cmek-service";
import { convertorServiceFactory } from "@app/services/convertor/convertor-service";
@@ -2122,6 +2123,16 @@ export const registerRoutes = async (
permissionService
});
const certificateV3Queue = certificateV3QueueServiceFactory({
queueService,
certificateDAL,
certificateAuthorityDAL,
certificateProfileDAL,
projectDAL,
certificateV3Service,
auditLogService
});
const certificateEstV3Service = certificateEstV3ServiceFactory({
internalCertificateAuthorityService,
certificateTemplateV2Service,
@@ -2281,6 +2292,7 @@ export const registerRoutes = async (
await dailyReminderQueueService.startSecretReminderMigrationJob();
await dailyExpiringPkiItemAlert.startSendingAlerts();
await pkiSubscriberQueue.startDailyAutoRenewalJob();
await certificateV3Queue.startDailyAutoRenewalJob();
await kmsService.startService();
await microsoftTeamsService.start();
await dynamicSecretQueueService.init();
@@ -42,7 +42,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
apiConfig: z
.object({
autoRenew: z.boolean().default(false),
autoRenewDays: z.number().min(1).max(365).optional()
renewBeforeDays: z.number().min(1).max(30).optional()
})
.optional()
})
@@ -150,7 +150,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
.object({
id: z.string(),
autoRenew: z.boolean(),
autoRenewDays: z.number().optional()
renewBeforeDays: z.number().optional()
})
.optional()
}).array(),
@@ -230,7 +230,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
.object({
id: z.string(),
autoRenew: z.boolean(),
autoRenewDays: z.number().optional()
renewBeforeDays: z.number().optional()
})
.optional(),
metrics: z
@@ -355,7 +355,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
apiConfig: z
.object({
autoRenew: z.boolean().default(false),
autoRenewDays: z.number().min(1).max(365).optional()
renewBeforeDays: z.number().min(1).max(30).optional()
})
.optional()
})
@@ -343,4 +343,138 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
return data;
}
});
server.route({
method: "POST",
url: "/:certificateId/renew",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificates],
params: z.object({
certificateId: z.string().uuid()
}),
response: {
200: z.object({
certificate: z.string().trim(),
issuingCaCertificate: z.string().trim(),
certificateChain: z.string().trim(),
privateKey: z.string().trim().optional(),
serialNumber: z.string().trim(),
certificateId: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const data = await server.services.certificateV3.renewCertificate({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
certificateId: req.params.certificateId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.RENEW_CERTIFICATE,
metadata: {
originalCertificateId: req.params.certificateId,
newCertificateId: data.certificateId,
profileName: data.profileName
}
}
});
return data;
}
});
server.route({
method: "PATCH",
url: "/:certificateId/config",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificates],
params: z.object({
certificateId: z.string().uuid()
}),
body: z.object({
renewBeforeDays: z.number().int().min(1).max(30).optional(),
disableAutoRenewal: z.boolean().optional()
}),
response: {
200: z.object({
message: z.string(),
renewBeforeDays: z.number().optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
if (req.body.disableAutoRenewal === true) {
const data = await server.services.certificateV3.disableRenewalConfig({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
certificateId: req.params.certificateId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.DISABLE_CERTIFICATE_RENEWAL_CONFIG,
metadata: {
certificateId: req.params.certificateId
}
}
});
return {
message: "Auto-renewal disabled successfully"
};
}
if (req.body.renewBeforeDays !== undefined) {
const data = await server.services.certificateV3.updateRenewalConfig({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
certificateId: req.params.certificateId,
renewBeforeDays: req.body.renewBeforeDays
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.UPDATE_CERTIFICATE_RENEWAL_CONFIG,
metadata: {
certificateId: req.params.certificateId,
renewBeforeDays: req.body.renewBeforeDays.toString()
}
}
});
return {
message: "Certificate configuration updated successfully",
renewBeforeDays: data.renewBeforeDays
};
}
return {
message: "No configuration changes requested"
};
}
});
};
@@ -1180,7 +1180,8 @@ export const internalCertificateAuthorityServiceFactory = ({
extendedKeyUsages,
signatureAlgorithm,
keyAlgorithm,
isFromProfile
isFromProfile,
internal = false
}: TIssueCertFromCaDTO) => {
let ca: TCertificateAuthorityWithAssociatedCa | undefined;
let certificateTemplate: TCertificateTemplates | undefined;
@@ -1210,19 +1211,21 @@ export const internalCertificateAuthorityServiceFactory = ({
throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: ca.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
if (!internal) {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: ca.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Create,
ProjectPermissionSub.Certificates
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Create,
ProjectPermissionSub.Certificates
);
}
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
if (!ca.internalCa.activeCaCertId)
@@ -1488,7 +1491,9 @@ export const internalCertificateAuthorityServiceFactory = ({
notAfter: notAfterDate,
keyUsages: selectedKeyUsages,
extendedKeyUsages: selectedExtendedKeyUsages,
projectId: ca!.projectId
projectId: ca!.projectId,
keyAlgorithm: effectiveKeyAlgorithm,
signatureAlgorithm: signatureAlgorithm || ca!.internalCa!.keyAlgorithm
},
tx
);
@@ -1917,7 +1922,9 @@ export const internalCertificateAuthorityServiceFactory = ({
notAfter: notAfterDate,
keyUsages: selectedKeyUsages,
extendedKeyUsages: selectedExtendedKeyUsages,
projectId: ca!.projectId
projectId: ca!.projectId,
keyAlgorithm: keyAlgorithm || ca!.internalCa!.keyAlgorithm,
signatureAlgorithm: signatureAlgorithm || ca!.internalCa!.keyAlgorithm
},
tx
);
@@ -139,6 +139,7 @@ export type TIssueCertFromCaDTO = {
signatureAlgorithm?: CertSignatureAlgorithm;
keyAlgorithm?: CertKeyAlgorithm;
isFromProfile?: boolean;
internal?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TSignCertFromCaDTO =
@@ -175,6 +175,14 @@ export enum CertSignatureAlgorithm {
ECDSA_SHA512 = "ECDSA-SHA512"
}
export const CERTIFICATE_RENEWAL_CONFIG = {
MIN_RENEW_BEFORE_DAYS: 1,
MAX_RENEW_BEFORE_DAYS: 30,
QUEUE_BATCH_SIZE: 100,
DAILY_CRON_SCHEDULE: "0 0 * * *",
QUEUE_START_DELAY_MS: 5000
} as const;
export const SAN_TYPE_OPTIONS = Object.values(CertSubjectAlternativeNameType);
export const KEY_USAGE_OPTIONS = Object.values(CertKeyUsageType);
export const EXTENDED_KEY_USAGE_OPTIONS = Object.values(CertExtendedKeyUsageType);
@@ -109,7 +109,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estConfigEncryptedCaChain"),
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigId"),
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenew"),
db.ref("autoRenewDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenewDays")
db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigRenewBeforeDays")
)
.where(`${TableName.PkiCertificateProfile}.id`, id)
.first();
@@ -132,7 +132,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
? ({
id: result.apiConfigId,
autoRenew: !!result.apiConfigAutoRenew,
autoRenewDays: result.apiConfigAutoRenewDays || undefined
renewBeforeDays: result.apiConfigRenewBeforeDays || undefined
} as TCertificateProfileWithConfigs["apiConfig"])
: undefined;
@@ -264,7 +264,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estEncryptedCaChain"),
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiId"),
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenew"),
db.ref("autoRenewDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenewDays")
db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiRenewBeforeDays")
);
if (includeMetrics) {
@@ -290,7 +290,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estEncryptedCaChain"),
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiId"),
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenew"),
db.ref("autoRenewDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenewDays"),
db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiRenewBeforeDays"),
db.raw("COUNT(certificates.id) as total_certificates"),
db.raw(
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? THEN 1 END) as active_certificates',
@@ -333,7 +333,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
? {
id: result.apiId as string,
autoRenew: !!result.apiAutoRenew,
autoRenewDays: (result.apiAutoRenewDays as number) || undefined
renewBeforeDays: (result.apiRenewBeforeDays as number) || undefined
}
: undefined;
@@ -25,7 +25,7 @@ export const createCertificateProfileSchema = z
apiConfig: z
.object({
autoRenew: z.boolean().default(false),
autoRenewDays: z.number().min(1).max(365).optional()
renewBeforeDays: z.number().min(1).max(30).optional()
})
.optional()
})
@@ -75,7 +75,7 @@ export const updateCertificateProfileSchema = z
apiConfig: z
.object({
autoRenew: z.boolean().default(false),
autoRenewDays: z.number().min(1).max(365).optional()
renewBeforeDays: z.number().min(1).max(30).optional()
})
.optional()
})
@@ -110,7 +110,7 @@ describe("CertificateProfileService", () => {
apiConfig: {
id: "api-config-123",
autoRenew: true,
autoRenewDays: 30
renewBeforeDays: 30
}
};
@@ -202,7 +202,7 @@ describe("CertificateProfileService", () => {
certificateTemplateId: "template-123",
apiConfig: {
autoRenew: true,
autoRenewDays: 30
renewBeforeDays: 30
}
};
@@ -323,7 +323,7 @@ describe("CertificateProfileService", () => {
certificateTemplateId: "template-123",
apiConfig: {
autoRenew: true,
autoRenewDays: 30
renewBeforeDays: 30
}
};
@@ -761,7 +761,7 @@ describe("CertificateProfileService", () => {
certificateTemplateId: "template-123",
apiConfig: {
autoRenew: true,
autoRenewDays: 30
renewBeforeDays: 30
}
};
@@ -786,7 +786,7 @@ describe("CertificateProfileService", () => {
certificateTemplateId: "template-123",
apiConfig: {
autoRenew: true,
autoRenewDays: 7
renewBeforeDays: 7
}
};
@@ -808,7 +808,7 @@ describe("CertificateProfileService", () => {
expect(mockApiEnrollmentConfigDAL.create).toHaveBeenCalledWith(
{
autoRenew: true,
autoRenewDays: 7
renewBeforeDays: 7
},
undefined
);
@@ -225,7 +225,7 @@ export const certificateProfileServiceFactory = ({
const apiConfig = await apiEnrollmentConfigDAL.create(
{
autoRenew: data.apiConfig.autoRenew,
autoRenewDays: data.apiConfig.autoRenewDays
renewBeforeDays: data.apiConfig.renewBeforeDays
},
tx
);
@@ -343,7 +343,7 @@ export const certificateProfileServiceFactory = ({
existingProfile.apiConfigId,
{
autoRenew: apiConfig.autoRenew,
autoRenewDays: apiConfig.autoRenewDays
renewBeforeDays: apiConfig.renewBeforeDays
},
tx
);
@@ -26,7 +26,7 @@ export type TCertificateProfileUpdate = Omit<TPkiCertificateProfilesUpdate, "enr
};
apiConfig?: {
autoRenew?: boolean;
autoRenewDays?: number;
renewBeforeDays?: number;
};
};
@@ -52,7 +52,7 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & {
apiConfig?: {
id: string;
autoRenew: boolean;
autoRenewDays?: number;
renewBeforeDays?: number;
};
metrics?: TCertificateProfileMetrics;
};
@@ -762,32 +762,36 @@ export const certificateTemplateV2ServiceFactory = ({
actorId,
actorAuthMethod,
actorOrgId,
templateId
templateId,
internal = false
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
templateId: string;
internal?: boolean;
}): Promise<TCertificateTemplateV2> => {
const template = await certificateTemplateV2DAL.findById(templateId);
if (!template) {
throw new NotFoundError({ message: "Certificate template not found" });
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: template.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
if (!internal) {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: template.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Read,
ProjectPermissionSub.CertificateTemplates
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Read,
ProjectPermissionSub.CertificateTemplates
);
}
return template;
};
@@ -0,0 +1,254 @@
/* eslint-disable no-await-in-loop */
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { ActorType } from "../auth/auth-type";
import { TCertificateDALFactory } from "../certificate/certificate-dal";
import { CertStatus } from "../certificate/certificate-types";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { CERTIFICATE_RENEWAL_CONFIG } from "../certificate-common/certificate-constants";
import { TCertificateProfileDALFactory } from "../certificate-profile/certificate-profile-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { TCertificateV3ServiceFactory } from "./certificate-v3-service";
type TCertificateV3QueueServiceFactoryDep = {
queueService: TQueueServiceFactory;
certificateDAL: TCertificateDALFactory;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">;
projectDAL: Pick<TProjectDALFactory, "findById">;
certificateV3Service: TCertificateV3ServiceFactory;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
};
export const certificateV3QueueServiceFactory = ({
queueService,
certificateDAL,
certificateAuthorityDAL,
certificateProfileDAL,
projectDAL,
certificateV3Service,
auditLogService
}: TCertificateV3QueueServiceFactoryDep) => {
queueService.start(QueueName.CertificateV3AutoRenewal, async (job) => {
if (job.name === QueueJobs.CertificateV3DailyAutoRenewal) {
logger.info(`${QueueJobs.CertificateV3DailyAutoRenewal}: queue task started`);
const { QUEUE_BATCH_SIZE } = CERTIFICATE_RENEWAL_CONFIG;
let offset = 0;
let hasMore = true;
while (hasMore) {
const certificates = await certificateDAL.find(
{
$notNull: ["profileId"],
status: CertStatus.ACTIVE,
renewedById: null,
renewalError: null,
revokedAt: null
},
{
limit: QUEUE_BATCH_SIZE,
offset
}
);
if (certificates.length === 0) {
hasMore = false;
break;
}
await Promise.all(
certificates.map(async (certificate) => {
try {
if (!certificate.profileId || !certificate.notAfter) {
return;
}
const profile = await certificateProfileDAL.findByIdWithConfigs(certificate.profileId);
if (!profile) {
logger.warn(`Profile not found for certificate ${certificate.id}`);
return;
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
if (!ca) {
logger.warn(`CA not found for certificate ${certificate.id}`);
return;
}
const profileAutoRenewEnabled = profile.apiConfig?.autoRenew === true;
const certificateHasRenewalConfig =
certificate.renewBeforeDays != null && certificate.renewBeforeDays > 0;
if (!profileAutoRenewEnabled && !certificateHasRenewalConfig) {
return;
}
const now = new Date();
if (certificate.notAfter <= now) {
return;
}
const renewBeforeDays = certificate.renewBeforeDays || profile.apiConfig?.renewBeforeDays;
if (!renewBeforeDays) {
return;
}
const { MIN_RENEW_BEFORE_DAYS, MAX_RENEW_BEFORE_DAYS } = CERTIFICATE_RENEWAL_CONFIG;
if (renewBeforeDays < MIN_RENEW_BEFORE_DAYS || renewBeforeDays > MAX_RENEW_BEFORE_DAYS) {
logger.warn(`Invalid renewal threshold ${renewBeforeDays} for certificate ${certificate.id}`);
return;
}
const expiryDate = new Date(certificate.notAfter);
const renewalDate = new Date(expiryDate.getTime() - renewBeforeDays * 24 * 60 * 60 * 1000);
const shouldRenew = renewalDate <= now;
if (shouldRenew) {
logger.info(`Auto-renewing certificate ${certificate.id} (common name: ${certificate.commonName})`);
const project = await projectDAL.findById(certificate.projectId);
if (!project) {
logger.error(`Project not found for certificate ${certificate.id}`);
return;
}
await certificateV3Service.renewCertificate({
actor: ActorType.PLATFORM,
actorId: "",
actorAuthMethod: null,
actorOrgId: project.orgId,
certificateId: certificate.id,
internal: true
});
await certificateDAL.updateById(certificate.id, {
renewalError: null
});
await auditLogService.createAuditLog({
projectId: certificate.projectId,
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
event: {
type: EventType.AUTOMATED_RENEW_CERTIFICATE,
metadata: {
certificateId: certificate.id,
commonName: certificate.commonName || "",
profileId: certificate.profileId,
renewBeforeDays: certificate.renewBeforeDays?.toString() || ""
}
}
});
logger.info(`Successfully auto-renewed certificate ${certificate.id}`);
}
} catch (error) {
logger.error(
error,
`Failed to auto-renew certificate ${certificate.id} (common name: ${certificate.commonName})`
);
const errorMessage = error instanceof Error ? error.message : "Unknown error";
let categorizedError = errorMessage;
if (errorMessage.includes("Template validation failed")) {
categorizedError =
"Auto-renewal failed: certificate template policy has changed and this certificate no longer meets the requirements";
} else if (errorMessage.includes("Certificate Authority") && errorMessage.includes("not found")) {
categorizedError =
"Auto-renewal failed: Certificate Authority for this certificate is no longer available";
} else if (errorMessage.includes("Certificate Authority is") && errorMessage.includes("must be ACTIVE")) {
categorizedError = "Auto-renewal failed: Certificate Authority is currently inactive";
} else if (errorMessage.includes("would expire") && errorMessage.includes("after its issuing CA")) {
categorizedError = "Auto-renewal failed: certificate would outlive the Certificate Authority";
} else if (
errorMessage.includes("TTL") &&
errorMessage.includes("must be greater than renewal threshold")
) {
categorizedError =
"Auto-renewal failed: certificate validity period is too short for the renewal threshold";
} else if (errorMessage.includes("not eligible for renewal")) {
categorizedError = "Auto-renewal failed: certificate is not eligible for automatic renewal";
} else if (errorMessage.includes("Requested validity period exceeds maximum allowed duration")) {
categorizedError =
"Auto-renewal failed: certificate validity period exceeds the maximum allowed by the profile template";
} else if (errorMessage.includes("not allowed by template policy")) {
categorizedError =
"Auto-renewal failed: certificate settings are no longer allowed by the profile template";
} else {
categorizedError = `Auto-renewal failed: ${errorMessage}`;
}
try {
await certificateDAL.updateById(certificate.id, {
renewalError: categorizedError
});
} catch (updateError) {
logger.error(updateError, `Failed to update renewal error for certificate ${certificate.id}`);
}
try {
await auditLogService.createAuditLog({
projectId: certificate.projectId,
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
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}`);
}
}
})
);
offset += QUEUE_BATCH_SIZE;
}
logger.info(`${QueueJobs.CertificateV3DailyAutoRenewal}: queue task completed`);
}
});
const startDailyAutoRenewalJob = async () => {
const { DAILY_CRON_SCHEDULE, QUEUE_START_DELAY_MS } = CERTIFICATE_RENEWAL_CONFIG;
await queueService.stopRepeatableJob(
QueueName.CertificateV3AutoRenewal,
QueueJobs.CertificateV3DailyAutoRenewal,
{ pattern: DAILY_CRON_SCHEDULE, utc: true },
QueueName.CertificateV3AutoRenewal
);
await queueService.queue(QueueName.CertificateV3AutoRenewal, QueueJobs.CertificateV3DailyAutoRenewal, undefined, {
delay: QUEUE_START_DELAY_MS,
jobId: QueueName.CertificateV3AutoRenewal,
repeat: { pattern: DAILY_CRON_SCHEDULE, utc: true }
});
};
queueService.listen(QueueName.CertificateV3AutoRenewal, "failed", (_, err) => {
logger.error(err, `${QueueName.CertificateV3AutoRenewal}: failed`);
});
return {
startDailyAutoRenewalJob
};
};
export type TCertificateV3QueueFactory = ReturnType<typeof certificateV3QueueServiceFactory>;
@@ -7,10 +7,11 @@ import { ForbiddenError } from "@casl/ability";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { ACMESANType, CertificateOrderStatus } from "@app/services/certificate/certificate-types";
import { ACMESANType, CertificateOrderStatus, CertStatus } from "@app/services/certificate/certificate-types";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums";
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
import {
CertExtendedKeyUsageType,
@@ -28,8 +29,9 @@ import { certificateV3ServiceFactory, TCertificateV3ServiceFactory } from "./cer
describe("CertificateV3Service", () => {
let service: TCertificateV3ServiceFactory;
const mockCertificateDAL: Pick<TCertificateDALFactory, "findOne" | "updateById"> = {
const mockCertificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById"> = {
findOne: vi.fn(),
findById: vi.fn(),
updateById: vi.fn()
};
@@ -1460,4 +1462,596 @@ describe("CertificateV3Service", () => {
).resolves.toBeDefined();
});
});
describe("renewCertificate", () => {
const mockOriginalCert = {
id: "cert-123",
status: CertStatus.ACTIVE,
serialNumber: "123456",
friendlyName: "Test Certificate",
commonName: "test.example.com",
notBefore: new Date("2024-01-01"),
notAfter: new Date("2024-02-01"), // 31 days
revokedAt: null,
renewedById: null,
profileId: "profile-123",
renewBeforeDays: 7,
caId: "ca-123",
pkiSubscriberId: null,
keyUsages: ["digital_signature", "key_agreement"],
extendedKeyUsages: ["server_auth"],
altNames: "test.example.com,api.example.com",
projectId: "project-123",
createdAt: new Date(),
updatedAt: new Date(),
certificateTemplateId: "template-123",
revocationReason: null,
caCertId: null,
renewedFromId: null,
renewalError: null,
keyAlgorithm: "RSA_2048",
signatureAlgorithm: "RSA-SHA256"
};
const mockProfile = {
id: "profile-123",
projectId: "project-123",
enrollmentType: EnrollmentType.API,
caId: "ca-123",
certificateTemplateId: "template-123",
apiConfig: {
id: "api-config-123",
autoRenew: true,
renewBeforeDays: 14
},
createdAt: new Date(),
updatedAt: new Date(),
slug: "test-profile",
description: "Test profile"
};
const mockCA = {
id: "ca-123",
projectId: "project-123",
status: CaStatus.ACTIVE,
createdAt: new Date(),
updatedAt: new Date(),
enableDirectIssuance: true,
name: "Test CA",
requireTemplateForIssuance: false,
externalCa: undefined,
parentCaId: null,
type: "ROOT",
friendlyName: "Test CA",
organization: "Test Org",
ou: "Test OU",
country: "US",
province: "CA",
locality: "SF",
commonName: "Test CA",
keyAlgorithm: "RSA_2048",
notAfter: "2025-01-01T00:00:00Z",
notBefore: "2024-01-01T00:00:00Z",
maxPathLength: -1,
activeCaCertId: "cert-123",
dn: "CN=Test CA,O=Test Org,OU=Test OU,C=US",
serialNumber: "123456789",
internalCa: {
id: "internal-ca-123",
parentCaId: null,
type: "ROOT",
friendlyName: "Test CA",
organization: "Test Org",
ou: "Test OU",
country: "US",
province: "CA",
locality: "SF",
commonName: "Test CA",
keyAlgorithm: "RSA_2048",
notAfter: "2025-01-01T00:00:00Z",
notBefore: "2024-01-01T00:00:00Z",
maxPathLength: -1,
activeCaCertId: "cert-123",
dn: "CN=Test CA,O=Test Org,OU=Test OU,C=US",
serialNumber: "123456789"
}
};
const mockTemplate = {
id: "template-123",
projectId: "project-123",
name: "Test Template",
createdAt: new Date(),
updatedAt: new Date(),
algorithms: {
signature: ["SHA256-RSA", "SHA384-RSA"],
keyType: ["RSA_2048", "RSA_4096"]
}
};
beforeEach(() => {
vi.clearAllMocks();
// Mock current date to be within renewal window
vi.useFakeTimers();
vi.setSystemTime(new Date("2024-01-26")); // 6 days before cert expires, within renewal window
});
afterEach(() => {
vi.useRealTimers();
});
it("should successfully renew eligible certificate", async () => {
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({
isValid: true,
errors: [],
warnings: []
});
vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue({
certificate: "renewed-cert",
certificateChain: "renewed-chain",
issuingCaCertificate: "issuing-ca",
privateKey: "private-key",
serialNumber: "789012",
ca: mockCA
});
const newCert = { ...mockOriginalCert, id: "cert-456", serialNumber: "789012" };
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(newCert);
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(newCert);
const result = await service.renewCertificate({
certificateId: "cert-123",
...mockActor
});
expect(result).toHaveProperty("certificate", "renewed-cert");
expect(result).toHaveProperty("certificateId", "cert-456");
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-456", {
profileId: "profile-123",
renewBeforeDays: 14,
renewedFromId: "cert-123"
});
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", {
renewedById: "cert-456",
renewalError: null
});
});
it("should validate certificate against current template during renewal", async () => {
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({
isValid: false,
errors: ["Subject alternative name not allowed"],
warnings: []
});
await expect(
service.renewCertificate({
certificateId: "cert-123",
...mockActor
})
).rejects.toThrow(BadRequestError);
await expect(
service.renewCertificate({
certificateId: "cert-123",
...mockActor
})
).rejects.toThrow(
"Certificate renewal failed because requested validity period exceeds maximum allowed duration by the profile template"
);
// Should store template validation error
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", {
renewalError: "Template validation failed: Subject alternative name not allowed"
});
});
it("should reject renewal if certificate is not from a profile", async () => {
const certWithoutProfile = { ...mockOriginalCert, profileId: null };
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(certWithoutProfile);
await expect(
service.renewCertificate({
certificateId: "cert-123",
...mockActor
})
).rejects.toThrow(ForbiddenRequestError);
await expect(
service.renewCertificate({
certificateId: "cert-123",
...mockActor
})
).rejects.toThrow("Only certificates issued from a profile can be renewed");
});
it("should reject renewal if certificate is already renewed", async () => {
const alreadyRenewedCert = { ...mockOriginalCert, renewedById: "cert-456" };
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(alreadyRenewedCert);
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
await expect(
service.renewCertificate({
certificateId: "cert-123",
...mockActor
})
).rejects.toThrow(BadRequestError);
await expect(
service.renewCertificate({
certificateId: "cert-123",
...mockActor
})
).rejects.toThrow("Certificate has already been renewed");
});
it("should reject renewal if certificate is expired", async () => {
const expiredCert = {
...mockOriginalCert,
notAfter: new Date("2024-01-20") // Expired 6 days ago
};
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(expiredCert);
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
await expect(
service.renewCertificate({
certificateId: "cert-123",
...mockActor
})
).rejects.toThrow(BadRequestError);
await expect(
service.renewCertificate({
certificateId: "cert-123",
...mockActor
})
).rejects.toThrow("Certificate is already expired");
});
it("should reject renewal if certificate is revoked", async () => {
const revokedCert = {
...mockOriginalCert,
revokedAt: new Date("2024-01-15")
};
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(revokedCert);
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
await expect(
service.renewCertificate({
certificateId: "cert-123",
...mockActor
})
).rejects.toThrow(BadRequestError);
await expect(
service.renewCertificate({
certificateId: "cert-123",
...mockActor
})
).rejects.toThrow("Certificate is revoked and cannot be renewed");
});
it("should reject renewal if CA is inactive", async () => {
const inactiveCA = { ...mockCA, status: CaStatus.DISABLED };
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(inactiveCA);
await expect(
service.renewCertificate({
certificateId: "cert-123",
...mockActor
})
).rejects.toThrow(BadRequestError);
await expect(
service.renewCertificate({
certificateId: "cert-123",
...mockActor
})
).rejects.toThrow("Certificate is not eligible for renewal: Certificate Authority is disabled, must be active");
});
it("should reject renewal if new certificate would outlive CA", async () => {
const shortLivedCA = {
...mockCA,
internalCa: {
...mockCA.internalCa,
notAfter: "2024-01-28T00:00:00Z"
}
};
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(shortLivedCA);
await expect(
service.renewCertificate({
certificateId: "cert-123",
...mockActor
})
).rejects.toThrow(BadRequestError);
await expect(
service.renewCertificate({
certificateId: "cert-123",
...mockActor
})
).rejects.toThrow("New certificate would expire");
});
it("should allow manual renewal outside window (manual renewal always bypasses window)", async () => {
vi.setSystemTime(new Date("2024-01-15")); // 17 days before expiry, outside 7-day window
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({
isValid: true,
errors: [],
warnings: []
});
vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue({
certificate: "renewed-cert",
certificateChain: "renewed-chain",
issuingCaCertificate: "issuing-ca",
privateKey: "private-key",
serialNumber: "789012",
ca: mockCA
});
const newCert = { ...mockOriginalCert, id: "cert-456", serialNumber: "789012" };
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(newCert);
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(newCert);
const result = await service.renewCertificate({
certificateId: "cert-123",
...mockActor
});
expect(result).toHaveProperty("certificate", "renewed-cert");
});
});
describe("updateRenewalConfig", () => {
it("should update renewal configuration successfully", async () => {
const mockCert = {
id: "cert-123",
profileId: "profile-123",
renewedById: null,
notBefore: new Date("2026-01-01"),
notAfter: new Date("2026-02-01"),
projectId: "project-123",
status: CertStatus.ACTIVE,
revokedAt: null
};
const mockProfile = {
id: "profile-123",
enrollmentType: EnrollmentType.API,
projectId: "project-123"
};
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCert as any);
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile as any);
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCert as any);
const result = await service.updateRenewalConfig({
actor: ActorType.USER,
actorId: "user-123",
actorAuthMethod: AuthMethod.EMAIL,
actorOrgId: "org-123",
certificateId: "cert-123",
renewBeforeDays: 7
});
expect(result).toEqual({
projectId: "project-123",
renewBeforeDays: 7
});
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", {
renewBeforeDays: 7
});
});
it("should reject update if certificate is not from profile", async () => {
const mockCert = {
id: "cert-123",
profileId: null,
renewedById: null,
projectId: "project-123"
};
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCert as any);
await expect(
service.updateRenewalConfig({
actor: ActorType.USER,
actorId: "user-123",
actorAuthMethod: AuthMethod.EMAIL,
actorOrgId: "org-123",
certificateId: "cert-123",
renewBeforeDays: 7
})
).rejects.toThrow(BadRequestError);
await expect(
service.updateRenewalConfig({
actor: ActorType.USER,
actorId: "user-123",
actorAuthMethod: AuthMethod.EMAIL,
actorOrgId: "org-123",
certificateId: "cert-123",
renewBeforeDays: 7
})
).rejects.toThrow("Certificate is not eligible for auto-renewal: certificate was not issued from a profile");
});
it("should reject update if certificate is already renewed", async () => {
const mockCert = {
id: "cert-123",
profileId: "profile-123",
renewedById: "cert-456",
projectId: "project-123",
status: CertStatus.ACTIVE,
revokedAt: null,
notBefore: new Date("2026-01-01"),
notAfter: new Date("2026-02-01")
};
const mockProfile = {
id: "profile-123",
enrollmentType: EnrollmentType.API,
projectId: "project-123"
};
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCert as any);
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile as any);
await expect(
service.updateRenewalConfig({
actor: ActorType.USER,
actorId: "user-123",
actorAuthMethod: AuthMethod.EMAIL,
actorOrgId: "org-123",
certificateId: "cert-123",
renewBeforeDays: 7
})
).rejects.toThrow(BadRequestError);
await expect(
service.updateRenewalConfig({
actor: ActorType.USER,
actorId: "user-123",
actorAuthMethod: AuthMethod.EMAIL,
actorOrgId: "org-123",
certificateId: "cert-123",
renewBeforeDays: 7
})
).rejects.toThrow("Certificate is not eligible for auto-renewal: certificate has already been renewed");
});
it("should reject update if renewBeforeDays >= certificate TTL", async () => {
const mockCert = {
id: "cert-123",
profileId: "profile-123",
renewedById: null,
notBefore: new Date("2026-01-01"),
notAfter: new Date("2026-01-08"),
projectId: "project-123",
status: CertStatus.ACTIVE,
revokedAt: null
};
const mockProfile = {
id: "profile-123",
enrollmentType: EnrollmentType.API,
projectId: "project-123"
};
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCert as any);
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile as any);
await expect(
service.updateRenewalConfig({
actor: ActorType.USER,
actorId: "user-123",
actorAuthMethod: AuthMethod.EMAIL,
actorOrgId: "org-123",
certificateId: "cert-123",
renewBeforeDays: 8 // Greater than 7-day TTL
})
).rejects.toThrow(BadRequestError);
await expect(
service.updateRenewalConfig({
actor: ActorType.USER,
actorId: "user-123",
actorAuthMethod: AuthMethod.EMAIL,
actorOrgId: "org-123",
certificateId: "cert-123",
renewBeforeDays: 8
})
).rejects.toThrow("Invalid renewal configuration: renewal threshold exceeds certificate validity period");
});
});
describe("disableRenewalConfig", () => {
it("should disable renewal configuration successfully", async () => {
const mockCert = {
id: "cert-123",
profileId: "profile-123",
projectId: "project-123"
};
const mockProfile = {
id: "profile-123",
enrollmentType: EnrollmentType.API,
projectId: "project-123"
};
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCert as any);
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile as any);
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCert as any);
const result = await service.disableRenewalConfig({
actor: ActorType.USER,
actorId: "user-123",
actorAuthMethod: AuthMethod.EMAIL,
actorOrgId: "org-123",
certificateId: "cert-123"
});
expect(result).toEqual({
projectId: "project-123"
});
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", {
renewBeforeDays: null
});
});
it("should reject disable if certificate is not from profile", async () => {
const mockCert = {
id: "cert-123",
profileId: null,
projectId: "project-123"
};
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCert as any);
await expect(
service.disableRenewalConfig({
actor: ActorType.USER,
actorId: "user-123",
actorAuthMethod: AuthMethod.EMAIL,
actorOrgId: "org-123",
certificateId: "cert-123"
})
).rejects.toThrow(BadRequestError);
await expect(
service.disableRenewalConfig({
actor: ActorType.USER,
actorId: "user-123",
actorAuthMethod: AuthMethod.EMAIL,
actorOrgId: "org-123",
certificateId: "cert-123"
})
).rejects.toThrow("Certificate is not eligible for auto-renewal: certificate was not issued from a profile");
});
});
});
@@ -1,9 +1,11 @@
import { ForbiddenError } from "@casl/ability";
import { randomUUID } from "crypto";
import RE2 from "re2";
import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import {
ProjectPermissionCertificateActions,
ProjectPermissionCertificateProfileActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
@@ -11,15 +13,18 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import {
CertExtendedKeyUsage,
CertificateOrderStatus,
CertKeyAlgorithm,
CertSignatureAlgorithm
CertKeyUsage,
CertSignatureAlgorithm,
CertStatus
} from "@app/services/certificate/certificate-types";
import {
TCertificateAuthorityDALFactory,
TCertificateAuthorityWithAssociatedCa
} from "@app/services/certificate-authority/certificate-authority-dal";
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
@@ -30,7 +35,9 @@ import {
bufferToString,
buildCertificateSubjectFromTemplate,
buildSubjectAlternativeNamesFromTemplate,
convertExtendedKeyUsageArrayFromLegacy,
convertExtendedKeyUsageArrayToLegacy,
convertKeyUsageArrayFromLegacy,
convertKeyUsageArrayToLegacy,
mapEnumsForValidation,
normalizeDateForApi
@@ -38,13 +45,18 @@ import {
import {
TCertificateFromProfileResponse,
TCertificateOrderResponse,
TDisableRenewalConfigDTO,
TDisableRenewalResponse,
TIssueCertificateFromProfileDTO,
TOrderCertificateFromProfileDTO,
TSignCertificateFromProfileDTO
TRenewalConfigResponse,
TRenewCertificateDTO,
TSignCertificateFromProfileDTO,
TUpdateRenewalConfigDTO
} from "./certificate-v3-types";
type TCertificateV3ServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "updateById">;
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">;
certificateTemplateV2Service: Pick<
@@ -95,6 +107,77 @@ const validateProfileAndPermissions = async (
return profile;
};
const validateRenewalEligibility = (
certificate: {
id: string;
status: string;
notBefore: Date;
notAfter: Date;
revokedAt?: Date | null;
renewedById?: string | null;
profileId?: string | null;
caId?: string | null;
pkiSubscriberId?: string | null;
},
ca: TCertificateAuthorityWithAssociatedCa
) => {
const errors: string[] = [];
if (certificate.status !== CertStatus.ACTIVE) {
errors.push(`Certificate status is ${certificate.status}, must be ${CertStatus.ACTIVE}`);
}
const now = new Date();
if (certificate.notAfter <= now) {
errors.push("Certificate is already expired");
}
if (certificate.revokedAt) {
errors.push("Certificate is revoked and cannot be renewed");
}
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
const isInternalCa = caType === CaType.INTERNAL;
const isConnectedExternalCa = caType === CaType.ACME || caType === CaType.AZURE_AD_CS;
const isImportedCertificate = certificate.pkiSubscriberId != null && !certificate.profileId;
if (!isInternalCa && !isConnectedExternalCa) {
errors.push(`CA type ${String(caType)} does not support renewal`);
}
if (isImportedCertificate) {
errors.push("Externally imported certificates cannot be renewed");
}
if (ca.status !== CaStatus.ACTIVE) {
errors.push(`Certificate Authority is ${ca.status}, must be ${CaStatus.ACTIVE}`);
}
if (certificate.renewedById) {
errors.push("Certificate has already been renewed");
}
const certificateTtlInDays = Math.ceil(
(certificate.notAfter.getTime() - certificate.notBefore.getTime()) / (24 * 60 * 60 * 1000)
);
if (ca.internalCa?.notAfter) {
const caExpiryDate = new Date(ca.internalCa.notAfter);
const proposedCertExpiryDate = new Date(now.getTime() + certificateTtlInDays * 24 * 60 * 60 * 1000);
if (proposedCertExpiryDate > caExpiryDate) {
errors.push(
`New certificate would expire (${proposedCertExpiryDate.toISOString()}) after its issuing CA (${caExpiryDate.toISOString()})`
);
}
}
return {
isEligible: errors.length === 0,
errors
};
};
const validateCaSupport = (ca: TCertificateAuthorityWithAssociatedCa, operation: string) => {
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
if (caType !== CaType.INTERNAL) {
@@ -155,6 +238,63 @@ const extractCertificateFromBuffer = (certData: Buffer | { rawData: Buffer } | s
return bufferToString(certData as unknown as Buffer);
};
const parseKeyUsages = (keyUsages: unknown): CertKeyUsage[] => {
if (!keyUsages) return [];
if (Array.isArray(keyUsages)) return keyUsages as CertKeyUsage[];
return (keyUsages as string).split(",").map((usage) => usage.trim() as CertKeyUsage);
};
const parseExtendedKeyUsages = (extendedKeyUsages: unknown): CertExtendedKeyUsage[] => {
if (!extendedKeyUsages) return [];
if (Array.isArray(extendedKeyUsages)) return extendedKeyUsages as CertExtendedKeyUsage[];
return (extendedKeyUsages as string).split(",").map((usage) => usage.trim() as CertExtendedKeyUsage);
};
const isValidRenewalTiming = (renewBeforeDays: number, certificateExpiryDate: Date): boolean => {
const renewalDate = new Date(certificateExpiryDate.getTime() - renewBeforeDays * 24 * 60 * 60 * 1000);
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0);
return renewalDate >= tomorrow;
};
const calculateRenewalThreshold = (
profileRenewBeforeDays: number | undefined,
certificateTtlInDays: number
): number | undefined => {
if (!profileRenewBeforeDays) {
return undefined;
}
if (certificateTtlInDays > profileRenewBeforeDays) {
return profileRenewBeforeDays;
}
return Math.max(1, certificateTtlInDays - 1);
};
const parseTtlToDays = (ttl: string): number => {
const match = ttl.match(new RE2("^(\\d+)([dhm])$"));
if (!match) {
throw new BadRequestError({ message: `Invalid TTL format: ${ttl}` });
}
const [, value, unit] = match;
const numValue = parseInt(value, 10);
switch (unit) {
case "d":
return numValue;
case "h":
return Math.ceil(numValue / 24);
case "m":
return Math.ceil(numValue / (24 * 60));
default:
throw new BadRequestError({ message: `Unsupported TTL unit: ${unit}` });
}
};
export const certificateV3ServiceFactory = ({
certificateDAL,
certificateAuthorityDAL,
@@ -274,7 +414,16 @@ export const certificateV3ServiceFactory = ({
throw new NotFoundError({ message: "Certificate was issued but could not be found in database" });
}
await certificateDAL.updateById(cert.id, { profileId });
const certificateTtlInDays = parseTtlToDays(certificateRequest.validity.ttl);
const renewBeforeDays = calculateRenewalThreshold(profile.apiConfig?.renewBeforeDays, certificateTtlInDays);
const finalRenewBeforeDays =
renewBeforeDays && isValidRenewalTiming(renewBeforeDays, new Date(cert.notAfter)) ? renewBeforeDays : undefined;
await certificateDAL.updateById(cert.id, {
profileId,
renewBeforeDays: finalRenewBeforeDays
});
return {
certificate: bufferToString(certificate),
@@ -371,7 +520,16 @@ export const certificateV3ServiceFactory = ({
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
}
await certificateDAL.updateById(cert.id, { profileId });
const certificateTtlInDays = parseTtlToDays(validity.ttl);
const renewBeforeDays = calculateRenewalThreshold(profile.apiConfig?.renewBeforeDays, certificateTtlInDays);
const finalRenewBeforeDays =
renewBeforeDays && isValidRenewalTiming(renewBeforeDays, new Date(cert.notAfter)) ? renewBeforeDays : undefined;
await certificateDAL.updateById(cert.id, {
profileId,
renewBeforeDays: finalRenewBeforeDays
});
const certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer);
const certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer);
@@ -479,9 +637,341 @@ export const certificateV3ServiceFactory = ({
});
};
const renewCertificate = async ({
certificateId,
actor,
actorId,
actorAuthMethod,
actorOrgId,
internal = false
}: TRenewCertificateDTO & { internal?: boolean }): Promise<TCertificateFromProfileResponse> => {
const originalCert = await certificateDAL.findById(certificateId);
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 =
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(
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 because requested validity period exceeds maximum allowed duration by the profile template: ${validationResult.errors.join(", ")}`
});
}
validateAlgorithmCompatibility(ca, template);
const notBefore = new Date();
const notAfter = new Date(Date.now() + parseTtlToDays(ttl) * 24 * 60 * 60 * 1000);
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
});
const newCert = await certificateDAL.findOne({ serialNumber, caId: ca.id });
if (!newCert) {
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
}
const certificateTtlInDays = parseTtlToDays(ttl);
const finalRenewBeforeDays = calculateRenewalThreshold(profile.apiConfig?.renewBeforeDays, certificateTtlInDays);
await certificateDAL.updateById(newCert.id, {
profileId: originalCert.profileId,
renewBeforeDays: finalRenewBeforeDays,
renewedFromId: originalCert.id
});
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 {
certificate: certificateString,
issuingCaCertificate: extractCertificateFromBuffer(issuingCaCertificate as unknown as Buffer),
certificateChain: certificateChainString,
serialNumber,
certificateId: newCert.id,
projectId: profile.projectId,
profileName: profile.slug
};
};
const updateRenewalConfig = async ({
certificateId,
renewBeforeDays,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TUpdateRenewalConfigDTO): Promise<TRenewalConfigResponse> => {
const certificate = await certificateDAL.findById(certificateId);
if (!certificate) {
throw new NotFoundError({ message: "Certificate not found" });
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: certificate.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Edit,
ProjectPermissionSub.Certificates
);
if (!certificate.profileId) {
throw new BadRequestError({
message: "Certificate is not eligible for auto-renewal: certificate was not issued from a profile"
});
}
const profile = await certificateProfileDAL.findByIdWithConfigs(certificate.profileId);
if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" });
}
if (profile.enrollmentType !== "api") {
throw new ForbiddenRequestError({
message: "Certificate is not eligible for auto-renewal: EST certificates cannot be auto-renewed"
});
}
if (certificate.status !== CertStatus.ACTIVE) {
throw new BadRequestError({
message: `Certificate is not eligible for auto-renewal: certificate status is ${certificate.status}, must be active`
});
}
const now = new Date();
if (certificate.notAfter <= now) {
throw new BadRequestError({
message: "Certificate is not eligible for auto-renewal: certificate has expired"
});
}
if (certificate.revokedAt) {
throw new BadRequestError({
message: "Certificate is not eligible for auto-renewal: certificate has been revoked"
});
}
if (certificate.renewedById) {
throw new BadRequestError({
message: "Certificate is not eligible for auto-renewal: certificate has already been renewed"
});
}
const certificateTtlInDays = Math.ceil(
(new Date(certificate.notAfter).getTime() - new Date(certificate.notBefore).getTime()) / (24 * 60 * 60 * 1000)
);
if (renewBeforeDays >= certificateTtlInDays) {
throw new BadRequestError({
message: "Invalid renewal configuration: renewal threshold exceeds certificate validity period"
});
}
if (!isValidRenewalTiming(renewBeforeDays, new Date(certificate.notAfter))) {
throw new BadRequestError({
message: "Invalid renewal configuration: renewal would be triggered immediately or in the past"
});
}
await certificateDAL.updateById(certificateId, {
renewBeforeDays
});
return {
projectId: certificate.projectId,
renewBeforeDays
};
};
const disableRenewalConfig = async ({
certificateId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TDisableRenewalConfigDTO): Promise<TDisableRenewalResponse> => {
const certificate = await certificateDAL.findById(certificateId);
if (!certificate) {
throw new NotFoundError({ message: "Certificate not found" });
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: certificate.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Edit,
ProjectPermissionSub.Certificates
);
if (!certificate.profileId) {
throw new BadRequestError({
message: "Certificate is not eligible for auto-renewal: certificate was not issued from a profile"
});
}
const profile = await certificateProfileDAL.findByIdWithConfigs(certificate.profileId);
if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" });
}
if (profile.enrollmentType !== "api") {
throw new ForbiddenRequestError({
message: "Certificate is not eligible for auto-renewal: EST certificates cannot be auto-renewed"
});
}
await certificateDAL.updateById(certificateId, {
renewBeforeDays: null
});
return {
projectId: certificate.projectId
};
};
return {
issueCertificateFromProfile,
signCertificateFromProfile,
orderCertificateFromProfile
orderCertificateFromProfile,
renewCertificate,
updateRenewalConfig,
disableRenewalConfig
};
};
@@ -97,3 +97,25 @@ export type TCertificateOrderResponse = {
projectId: string;
profileName: string;
};
export type TRenewCertificateDTO = {
certificateId: string;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateRenewalConfigDTO = {
certificateId: string;
renewBeforeDays: number;
} & Omit<TProjectPermission, "projectId">;
export type TDisableRenewalConfigDTO = {
certificateId: string;
} & Omit<TProjectPermission, "projectId">;
export type TRenewalConfigResponse = {
projectId: string;
renewBeforeDays: number;
};
export type TDisableRenewalResponse = {
projectId: string;
};
@@ -69,15 +69,15 @@ export const apiEnrollmentConfigDALFactory = (db: TDbClient) => {
const profiles = await query
.where((qb) => {
void qb
.whereNull(`${TableName.PkiApiEnrollmentConfig}.autoRenewDays`)
.orWhere(`${TableName.PkiApiEnrollmentConfig}.autoRenewDays`, "<=", renewalThresholdDays);
.whereNull(`${TableName.PkiApiEnrollmentConfig}.renewBeforeDays`)
.orWhere(`${TableName.PkiApiEnrollmentConfig}.renewBeforeDays`, "<=", renewalThresholdDays);
})
.select((tx || db).ref("id").withSchema(TableName.PkiCertificateProfile))
.select((tx || db).ref("name").withSchema(TableName.PkiCertificateProfile))
.select((tx || db).ref("projectId").withSchema(TableName.PkiCertificateProfile))
.select((tx || db).ref("autoRenewDays").withSchema(TableName.PkiCertificateProfile));
.select((tx || db).ref("renewBeforeDays").withSchema(TableName.PkiCertificateProfile));
return profiles as Array<{ id: string; name: string; projectId: string; autoRenewDays?: number }>;
return profiles as Array<{ id: string; name: string; projectId: string; renewBeforeDays?: number }>;
} catch (error) {
throw new DatabaseError({ error, name: "Find profiles for auto renewal" });
}
@@ -25,5 +25,5 @@ export interface TEstConfigData {
export interface TApiConfigData {
autoRenew: boolean;
autoRenewDays?: number;
renewBeforeDays?: number;
}