misc: endpoint for fetching latest active bundle of profile

This commit is contained in:
Sheen Capadngan
2025-11-08 03:04:00 +08:00
parent 8ee71ba585
commit 2b67fe7608
4 changed files with 242 additions and 62 deletions
@@ -462,6 +462,24 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
}
};
const getLatestActiveCertificateForProfile = async (profileId: string, tx?: Knex) => {
try {
const now = new Date();
const certificate = await (tx || db)(TableName.Certificate)
.where("profileId", profileId)
.where("status", "active")
.where("notAfter", ">", now)
.whereNull("revokedAt")
.orderBy("createdAt", "desc")
.first();
return certificate;
} catch (error) {
throw new DatabaseError({ error, name: "Get latest active certificate by profile" });
}
};
const isProfileInUse = async (profileId: string, tx?: Knex) => {
try {
const doc = await (tx || db)(TableName.Certificate).where("profileId", profileId).count("*").first();
@@ -485,6 +503,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
countByProjectId,
findByNameAndProjectId,
getCertificatesByProfile,
getLatestActiveCertificateForProfile,
isProfileInUse
};
};
@@ -4,17 +4,24 @@ import * as x509 from "@peculiar/x509";
import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import {
ProjectPermissionCertificateActions,
ProjectPermissionCertificateProfileActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { buildUrl } from "@app/ee/services/pki-acme/pki-acme-fns";
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal";
import { getCertificateCredentials, isCertChainValid } from "../certificate/certificate-fns";
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal";
import { isCertChainValid } from "../certificate/certificate-fns";
import { TAcmeEnrollmentConfigDALFactory } from "../enrollment-config/acme-enrollment-config-dal";
import { TApiEnrollmentConfigDALFactory } from "../enrollment-config/api-enrollment-config-dal";
import { TAcmeConfigData, TApiConfigData, TEstConfigData } from "../enrollment-config/enrollment-config-types";
import { TEstEnrollmentConfigDALFactory } from "../enrollment-config/est-enrollment-config-dal";
@@ -30,8 +37,6 @@ import {
TCertificateProfileUpdate,
TCertificateProfileWithConfigs
} from "./certificate-profile-types";
import { TAcmeEnrollmentConfigDALFactory } from "../enrollment-config/acme-enrollment-config-dal";
import { buildUrl } from "@app/ee/services/pki-acme/pki-acme-fns";
const generateAndEncryptAcmeEabSecret = async (
projectId: string,
@@ -142,6 +147,10 @@ type TCertificateProfileServiceFactoryDep = {
apiEnrollmentConfigDAL: TApiEnrollmentConfigDALFactory;
estEnrollmentConfigDAL: TEstEnrollmentConfigDALFactory;
acmeEnrollmentConfigDAL: TAcmeEnrollmentConfigDALFactory;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
@@ -162,6 +171,8 @@ export const certificateProfileServiceFactory = ({
apiEnrollmentConfigDAL,
estEnrollmentConfigDAL,
acmeEnrollmentConfigDAL,
certificateBodyDAL,
certificateSecretDAL,
permissionService,
kmsService,
projectDAL
@@ -729,6 +740,106 @@ export const certificateProfileServiceFactory = ({
return certificates;
};
const getLatestActiveCertificateBundle = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
profileId
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
profileId: string;
}) => {
const profile = await certificateProfileDAL.findById(profileId);
if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" });
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: profile.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.Read,
ProjectPermissionSub.CertificateProfiles
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.ReadPrivateKey,
ProjectPermissionSub.Certificates
);
const cert = await certificateProfileDAL.getLatestActiveCertificateForProfile(profileId);
if (!cert) {
return null;
}
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
projectId: cert.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKeyId
});
const decryptedCert = await kmsDecryptor({
cipherTextBlob: certBody.encryptedCertificate
});
// @ts-expect-error - Buffer type is compatible at runtime despite TypeScript error
const certObj = new x509.X509Certificate(decryptedCert);
const certificate = certObj.toString("pem");
const decryptedCertChain = await kmsDecryptor({
cipherTextBlob: certBody.encryptedCertificateChain!
});
const certificateChain = decryptedCertChain.toString();
let privateKey = null;
try {
const { certPrivateKey } = await getCertificateCredentials({
certId: cert.id,
projectId: cert.projectId,
certificateSecretDAL,
projectDAL,
kmsService
});
privateKey = certPrivateKey;
} catch (error) {
// Private key might not exist for ACME certificates or other external workflows
// where the key is generated client-side
if (error instanceof NotFoundError) {
privateKey = null;
} else {
throw error;
}
}
return {
certificate,
certificateChain,
privateKey,
serialNumber: cert.serialNumber
};
};
const getEstConfigurationByProfile = async (
params:
| {
@@ -854,6 +965,7 @@ export const certificateProfileServiceFactory = ({
listProfiles,
deleteProfile,
getProfileCertificates,
getLatestActiveCertificateBundle,
getEstConfigurationByProfile,
revealAcmeEabSecret
};