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