mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-19 07:33:18 +00:00
Address PR suggestions
This commit is contained in:
@@ -6,7 +6,6 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||
import { CERTIFICATE_RENEWAL_CONFIG } from "../certificate-common/certificate-constants";
|
||||
import { categorizeCertificateRenewalError } from "../certificate-common/certificate-utils";
|
||||
import { TCertificateV3ServiceFactory } from "./certificate-v3-service";
|
||||
|
||||
type TCertificateV3QueueServiceFactoryDep = {
|
||||
@@ -70,9 +69,6 @@ export const certificateV3QueueServiceFactory = ({
|
||||
internal: true
|
||||
});
|
||||
|
||||
await certificateDAL.updateById(certificate.id, {
|
||||
renewalError: null
|
||||
});
|
||||
totalCertificatesRenewed += 1;
|
||||
|
||||
await auditLogService.createAuditLog({
|
||||
@@ -87,42 +83,32 @@ export const certificateV3QueueServiceFactory = ({
|
||||
certificateId: certificate.id,
|
||||
commonName: certificate.commonName || "",
|
||||
profileId: certificate.profileId!,
|
||||
renewBeforeDays: certificate.renewBeforeDays?.toString() || ""
|
||||
renewBeforeDays: certificate.renewBeforeDays?.toString() || "",
|
||||
profileName: certificate.profileName || ""
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
const categorizedError: string = categorizeCertificateRenewalError(error);
|
||||
|
||||
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
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.error(error, `Failed to renew certificate ${certificate.id}: ${errorMessage}`);
|
||||
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() || "",
|
||||
profileName: certificate.profileName || "",
|
||||
error: errorMessage
|
||||
}
|
||||
});
|
||||
} 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", () => {
|
||||
let service: TCertificateV3ServiceFactory;
|
||||
|
||||
const mockCertificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById"> = {
|
||||
const mockCertificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction"> = {
|
||||
findOne: 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"> = {
|
||||
@@ -78,7 +82,7 @@ describe("CertificateV3Service", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks before each test
|
||||
vi.clearAllMocks();
|
||||
vi.resetAllMocks();
|
||||
|
||||
// Mock ForbiddenError.from static method
|
||||
vi.spyOn(ForbiddenError, "from").mockReturnValue({
|
||||
@@ -1473,7 +1477,7 @@ describe("CertificateV3Service", () => {
|
||||
notBefore: new Date("2024-01-01"),
|
||||
notAfter: new Date("2024-02-01"), // 31 days
|
||||
revokedAt: null,
|
||||
renewedById: null,
|
||||
renewedByCertificateId: null,
|
||||
profileId: "profile-123",
|
||||
renewBeforeDays: 7,
|
||||
caId: "ca-123",
|
||||
@@ -1487,7 +1491,7 @@ describe("CertificateV3Service", () => {
|
||||
certificateTemplateId: "template-123",
|
||||
revocationReason: null,
|
||||
caCertId: null,
|
||||
renewedFromId: null,
|
||||
renewedFromCertificateId: null,
|
||||
renewalError: null,
|
||||
keyAlgorithm: "RSA_2048",
|
||||
signatureAlgorithm: "RSA-SHA256"
|
||||
@@ -1570,8 +1574,6 @@ describe("CertificateV3Service", () => {
|
||||
};
|
||||
|
||||
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
|
||||
@@ -1582,6 +1584,7 @@ describe("CertificateV3Service", () => {
|
||||
});
|
||||
|
||||
it("should successfully renew eligible certificate", async () => {
|
||||
// Mock the initial findById call
|
||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
|
||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
||||
@@ -1604,6 +1607,13 @@ describe("CertificateV3Service", () => {
|
||||
vi.mocked(mockCertificateDAL.findOne).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({
|
||||
certificateId: "cert-123",
|
||||
...mockActor
|
||||
@@ -1611,15 +1621,23 @@ describe("CertificateV3Service", () => {
|
||||
|
||||
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
|
||||
});
|
||||
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith(
|
||||
"cert-456",
|
||||
{
|
||||
profileId: "profile-123",
|
||||
renewBeforeDays: 14,
|
||||
renewedFromCertificateId: "cert-123"
|
||||
},
|
||||
{}
|
||||
);
|
||||
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith(
|
||||
"cert-123",
|
||||
{
|
||||
renewedByCertificateId: "cert-456",
|
||||
renewalError: null
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it("should validate certificate against current template during renewal", async () => {
|
||||
@@ -1633,6 +1651,15 @@ describe("CertificateV3Service", () => {
|
||||
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(
|
||||
service.renewCertificate({
|
||||
certificateId: "cert-123",
|
||||
@@ -1645,9 +1672,7 @@ describe("CertificateV3Service", () => {
|
||||
certificateId: "cert-123",
|
||||
...mockActor
|
||||
})
|
||||
).rejects.toThrow(
|
||||
"Certificate renewal failed because requested validity period exceeds maximum allowed duration by the profile template: Subject alternative name not allowed"
|
||||
);
|
||||
).rejects.toThrow("Certificate renewal failed. Errors: Subject alternative name not allowed");
|
||||
|
||||
// Should store template validation error
|
||||
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", {
|
||||
@@ -1659,6 +1684,12 @@ describe("CertificateV3Service", () => {
|
||||
const certWithoutProfile = { ...mockOriginalCert, profileId: null };
|
||||
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(
|
||||
service.renewCertificate({
|
||||
certificateId: "cert-123",
|
||||
@@ -1675,11 +1706,20 @@ describe("CertificateV3Service", () => {
|
||||
});
|
||||
|
||||
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(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||
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(
|
||||
service.renewCertificate({
|
||||
certificateId: "cert-123",
|
||||
@@ -1704,6 +1744,15 @@ describe("CertificateV3Service", () => {
|
||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||
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(
|
||||
service.renewCertificate({
|
||||
certificateId: "cert-123",
|
||||
@@ -1728,6 +1777,15 @@ describe("CertificateV3Service", () => {
|
||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||
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(
|
||||
service.renewCertificate({
|
||||
certificateId: "cert-123",
|
||||
@@ -1749,6 +1807,15 @@ describe("CertificateV3Service", () => {
|
||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||
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(
|
||||
service.renewCertificate({
|
||||
certificateId: "cert-123",
|
||||
@@ -1776,6 +1843,15 @@ describe("CertificateV3Service", () => {
|
||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||
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(
|
||||
service.renewCertificate({
|
||||
certificateId: "cert-123",
|
||||
@@ -1816,6 +1892,12 @@ describe("CertificateV3Service", () => {
|
||||
vi.mocked(mockCertificateDAL.findOne).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({
|
||||
certificateId: "cert-123",
|
||||
...mockActor
|
||||
@@ -1830,12 +1912,13 @@ describe("CertificateV3Service", () => {
|
||||
const mockCert = {
|
||||
id: "cert-123",
|
||||
profileId: "profile-123",
|
||||
renewedById: null,
|
||||
renewedByCertificateId: null,
|
||||
notBefore: new Date("2026-01-01"),
|
||||
notAfter: new Date("2026-02-01"),
|
||||
projectId: "project-123",
|
||||
status: CertStatus.ACTIVE,
|
||||
revokedAt: null
|
||||
revokedAt: null,
|
||||
commonName: ""
|
||||
};
|
||||
|
||||
const mockProfile = {
|
||||
@@ -1859,7 +1942,8 @@ describe("CertificateV3Service", () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
projectId: "project-123",
|
||||
renewBeforeDays: 7
|
||||
renewBeforeDays: 7,
|
||||
commonName: ""
|
||||
});
|
||||
|
||||
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", {
|
||||
@@ -1871,7 +1955,7 @@ describe("CertificateV3Service", () => {
|
||||
const mockCert = {
|
||||
id: "cert-123",
|
||||
profileId: null,
|
||||
renewedById: null,
|
||||
renewedByCertificateId: null,
|
||||
projectId: "project-123"
|
||||
};
|
||||
|
||||
@@ -1904,7 +1988,7 @@ describe("CertificateV3Service", () => {
|
||||
const mockCert = {
|
||||
id: "cert-123",
|
||||
profileId: "profile-123",
|
||||
renewedById: "cert-456",
|
||||
renewedByCertificateId: "cert-456",
|
||||
projectId: "project-123",
|
||||
status: CertStatus.ACTIVE,
|
||||
revokedAt: null,
|
||||
@@ -1948,7 +2032,7 @@ describe("CertificateV3Service", () => {
|
||||
const mockCert = {
|
||||
id: "cert-123",
|
||||
profileId: "profile-123",
|
||||
renewedById: null,
|
||||
renewedByCertificateId: null,
|
||||
notBefore: new Date("2026-01-01"),
|
||||
notAfter: new Date("2026-01-08"),
|
||||
projectId: "project-123",
|
||||
@@ -1994,7 +2078,8 @@ describe("CertificateV3Service", () => {
|
||||
const mockCert = {
|
||||
id: "cert-123",
|
||||
profileId: "profile-123",
|
||||
projectId: "project-123"
|
||||
projectId: "project-123",
|
||||
commonName: ""
|
||||
};
|
||||
|
||||
const mockProfile = {
|
||||
@@ -2016,7 +2101,8 @@ describe("CertificateV3Service", () => {
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
projectId: "project-123"
|
||||
projectId: "project-123",
|
||||
commonName: ""
|
||||
});
|
||||
|
||||
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
CertExtendedKeyUsage,
|
||||
CertificateOrderStatus,
|
||||
CertKeyAlgorithm,
|
||||
CertKeyType,
|
||||
CertKeyUsage,
|
||||
CertSignatureAlgorithm,
|
||||
CertStatus
|
||||
@@ -56,7 +57,7 @@ import {
|
||||
} from "./certificate-v3-types";
|
||||
|
||||
type TCertificateV3ServiceFactoryDep = {
|
||||
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
|
||||
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">;
|
||||
certificateTemplateV2Service: Pick<
|
||||
@@ -114,7 +115,7 @@ const validateRenewalEligibility = (
|
||||
notBefore: Date;
|
||||
notAfter: Date;
|
||||
revokedAt?: Date | null;
|
||||
renewedById?: string | null;
|
||||
renewedByCertificateId?: string | null;
|
||||
profileId?: string | null;
|
||||
caId?: string | null;
|
||||
pkiSubscriberId?: string | null;
|
||||
@@ -153,7 +154,7 @@ const validateRenewalEligibility = (
|
||||
errors.push(`Certificate Authority is ${ca.status}, must be ${CaStatus.ACTIVE}`);
|
||||
}
|
||||
|
||||
if (certificate.renewedById) {
|
||||
if (certificate.renewedByCertificateId) {
|
||||
errors.push("Certificate has already been renewed");
|
||||
}
|
||||
|
||||
@@ -212,11 +213,11 @@ const validateAlgorithmCompatibility = (
|
||||
const keyType = parts[parts.length - 1];
|
||||
|
||||
if (caKeyAlgorithm.startsWith("RSA")) {
|
||||
return keyType === "RSA";
|
||||
return keyType === CertKeyType.RSA;
|
||||
}
|
||||
|
||||
if (caKeyAlgorithm.startsWith("EC")) {
|
||||
return keyType === "ECDSA";
|
||||
return keyType === CertKeyType.ECDSA;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -338,7 +339,8 @@ export const certificateV3ServiceFactory = ({
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
templateId: profile.certificateTemplateId
|
||||
templateId: profile.certificateTemplateId,
|
||||
internal: true
|
||||
});
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
||||
@@ -362,10 +364,6 @@ export const certificateV3ServiceFactory = ({
|
||||
|
||||
validateCaSupport(ca, "direct certificate issuance");
|
||||
|
||||
if (!actorAuthMethod) {
|
||||
throw new BadRequestError({ message: "Authentication method is required for certificate issuance" });
|
||||
}
|
||||
|
||||
validateAlgorithmCompatibility(ca, template);
|
||||
|
||||
const effectiveSignatureAlgorithm = certificateRequest.signatureAlgorithm as CertSignatureAlgorithm | undefined;
|
||||
@@ -433,7 +431,8 @@ export const certificateV3ServiceFactory = ({
|
||||
serialNumber,
|
||||
certificateId: cert.id,
|
||||
projectId: profile.projectId,
|
||||
profileName: profile.slug
|
||||
profileName: profile.slug,
|
||||
commonName: cert.commonName || ""
|
||||
};
|
||||
};
|
||||
|
||||
@@ -468,16 +467,13 @@ export const certificateV3ServiceFactory = ({
|
||||
|
||||
validateCaSupport(ca, "CSR signing");
|
||||
|
||||
if (!actorAuthMethod) {
|
||||
throw new BadRequestError({ message: "Authentication method is required for certificate signing" });
|
||||
}
|
||||
|
||||
const template = await certificateTemplateV2Service.getTemplateV2ById({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
templateId: profile.certificateTemplateId
|
||||
templateId: profile.certificateTemplateId,
|
||||
internal: true
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
@@ -541,7 +537,8 @@ export const certificateV3ServiceFactory = ({
|
||||
serialNumber,
|
||||
certificateId: cert.id,
|
||||
projectId: profile.projectId,
|
||||
profileName: profile.slug
|
||||
profileName: profile.slug,
|
||||
commonName: cert.commonName || ""
|
||||
};
|
||||
};
|
||||
|
||||
@@ -645,178 +642,224 @@ export const certificateV3ServiceFactory = ({
|
||||
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 =
|
||||
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 renewalResult = await certificateDAL.transaction(async (tx) => {
|
||||
const originalCert = await certificateDAL.findById(certificateId, tx);
|
||||
if (!originalCert) {
|
||||
throw new NotFoundError({ message: "Certificate not found" });
|
||||
}
|
||||
};
|
||||
|
||||
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
||||
profile.certificateTemplateId,
|
||||
certificateRequest
|
||||
);
|
||||
if (!originalCert.profileId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Only certificates issued from a profile can be renewed"
|
||||
});
|
||||
}
|
||||
|
||||
if (!validationResult.isValid) {
|
||||
await certificateDAL.updateById(originalCert.id, {
|
||||
renewalError: `Template validation failed: ${validationResult.errors.join(", ")}`
|
||||
});
|
||||
const originalSignatureAlgorithm = originalCert.signatureAlgorithm as CertSignatureAlgorithm;
|
||||
const originalKeyAlgorithm = originalCert.keyAlgorithm as CertKeyAlgorithm;
|
||||
|
||||
throw new BadRequestError({
|
||||
message: `Certificate renewal failed because requested validity period exceeds maximum allowed duration by the profile template: ${validationResult.errors.join(", ")}`
|
||||
});
|
||||
}
|
||||
if (!originalSignatureAlgorithm || !originalKeyAlgorithm) {
|
||||
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 notBefore = new Date();
|
||||
const notAfter = new Date(Date.now() + parseTtlToDays(ttl) * 24 * 60 * 60 * 1000);
|
||||
const profile = await certificateProfileDAL.findByIdWithConfigs(originalCert.profileId);
|
||||
if (!profile) {
|
||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||
}
|
||||
|
||||
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,
|
||||
if (profile.enrollmentType !== EnrollmentType.API) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Certificate is not eligible for renewal: EST certificates cannot be renewed through this endpoint"
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
templateId: profile.certificateTemplateId,
|
||||
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" });
|
||||
}
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
||||
}
|
||||
|
||||
const certificateTtlInDays = parseTtlToDays(ttl);
|
||||
const finalRenewBeforeDays = calculateRenewalThreshold(profile.apiConfig?.renewBeforeDays, certificateTtlInDays);
|
||||
const originalTtlInDays = Math.ceil(
|
||||
(new Date(originalCert.notAfter).getTime() - new Date(originalCert.notBefore).getTime()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
const ttl = `${originalTtlInDays}d`;
|
||||
|
||||
await certificateDAL.updateById(newCert.id, {
|
||||
profileId: originalCert.profileId,
|
||||
renewBeforeDays: finalRenewBeforeDays,
|
||||
renewedFromId: originalCert.id
|
||||
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 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 {
|
||||
certificate: certificateString,
|
||||
issuingCaCertificate: extractCertificateFromBuffer(issuingCaCertificate as unknown as Buffer),
|
||||
certificateChain: certificateChainString,
|
||||
serialNumber,
|
||||
certificateId: newCert.id,
|
||||
projectId: profile.projectId,
|
||||
profileName: profile.slug
|
||||
certificate: renewalResult.certificate,
|
||||
issuingCaCertificate: renewalResult.issuingCaCertificate,
|
||||
certificateChain: renewalResult.certificateChain,
|
||||
serialNumber: renewalResult.serialNumber,
|
||||
certificateId: renewalResult.newCert.id,
|
||||
projectId: renewalResult.profile.projectId,
|
||||
profileName: renewalResult.profile.slug,
|
||||
commonName: renewalResult.originalCert.commonName || ""
|
||||
};
|
||||
};
|
||||
|
||||
@@ -858,7 +901,7 @@ export const certificateV3ServiceFactory = ({
|
||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||
}
|
||||
|
||||
if (profile.enrollmentType !== "api") {
|
||||
if (profile.enrollmentType !== EnrollmentType.API) {
|
||||
throw new ForbiddenRequestError({
|
||||
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({
|
||||
message: "Certificate is not eligible for auto-renewal: certificate has already been renewed"
|
||||
});
|
||||
@@ -911,7 +954,8 @@ export const certificateV3ServiceFactory = ({
|
||||
|
||||
return {
|
||||
projectId: certificate.projectId,
|
||||
renewBeforeDays
|
||||
renewBeforeDays,
|
||||
commonName: certificate.commonName || ""
|
||||
};
|
||||
};
|
||||
|
||||
@@ -952,7 +996,7 @@ export const certificateV3ServiceFactory = ({
|
||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||
}
|
||||
|
||||
if (profile.enrollmentType !== "api") {
|
||||
if (profile.enrollmentType !== EnrollmentType.API) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Certificate is not eligible for auto-renewal: EST certificates cannot be auto-renewed"
|
||||
});
|
||||
@@ -963,7 +1007,8 @@ export const certificateV3ServiceFactory = ({
|
||||
});
|
||||
|
||||
return {
|
||||
projectId: certificate.projectId
|
||||
projectId: certificate.projectId,
|
||||
commonName: certificate.commonName || ""
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ export type TCertificateFromProfileResponse = {
|
||||
certificateId: string;
|
||||
projectId: string;
|
||||
profileName: string;
|
||||
commonName: string;
|
||||
};
|
||||
|
||||
export type TCertificateOrderResponse = {
|
||||
@@ -114,8 +115,10 @@ export type TDisableRenewalConfigDTO = {
|
||||
export type TRenewalConfigResponse = {
|
||||
projectId: string;
|
||||
renewBeforeDays: number;
|
||||
commonName: string;
|
||||
};
|
||||
|
||||
export type TDisableRenewalResponse = {
|
||||
projectId: string;
|
||||
commonName: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user