From e4a38e4ba8366e5ccae6e84dd84db571b26242bd Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 16:23:11 -0800 Subject: [PATCH 01/24] Add PKI acme license feature flag --- backend/src/ee/services/license/license-fns.ts | 1 + backend/src/ee/services/license/license-types.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 97061e3ca..14b7bcfbd 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -56,6 +56,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ secretsLimit: 40 }, pkiEst: false, + pkiAcme: false, enforceMfa: false, projectTemplates: false, kmip: false, diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index c4ff6a8fa..659067957 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -78,6 +78,7 @@ export type TFeatureSet = { secretsLimit: number; }; pkiEst: boolean; + pkiAcme: boolean; enforceMfa: boolean; projectTemplates: false; kmip: false; From ce6727460f24102e83e0f6bcec176dba6c4267bb Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 16:59:38 -0800 Subject: [PATCH 02/24] Block user if they don't have the feature in their plan --- .../CertificateProfilesTab.tsx | 21 +++++++++++-- .../CreateProfileModal.tsx | 31 ++++++++++++++++--- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx index ab34abec8..c58a2fe20 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx @@ -1,6 +1,6 @@ -import { useState } from "react"; import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useState } from "react"; import { createNotification } from "@app/components/notifications"; import { Button, DeleteActionModal } from "@app/components/v2"; @@ -14,6 +14,8 @@ import { useDeleteCertificateProfile } from "@app/hooks/api/certificateProfiles"; +import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; +import { usePopUp } from "@app/hooks"; import { CreateProfileModal } from "./CreateProfileModal"; import { ProfileList } from "./ProfileList"; import { RevealAcmeEabSecretModal } from "./RevealAcmeEabSecretModal"; @@ -29,6 +31,10 @@ export const CertificateProfilesTab = () => { const [selectedProfile, setSelectedProfile] = useState( null ); + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "enrollmentOptions", + "upgradePlan" + ] as const); const deleteProfile = useDeleteCertificateProfile(); @@ -99,7 +105,17 @@ export const CertificateProfilesTab = () => { onDeleteProfile={handleDeleteProfile} /> - setIsCreateModalOpen(false)} /> + setIsCreateModalOpen(false)} + handlePopUpOpen={handlePopUpOpen} + /> + handlePopUpToggle("upgradePlan", isOpen)} + isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature} + text="Your current plan does not include access to managing template enrollment options for ACME. To unlock this feature, please upgrade to Infisical Enterprise plan." + /> {selectedProfile && ( <> @@ -109,6 +125,7 @@ export const CertificateProfilesTab = () => { setIsEditModalOpen(false); setSelectedProfile(null); }} + handlePopUpOpen={handlePopUpOpen} profile={selectedProfile} mode="edit" /> diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx index f53084a4e..3214b1bb1 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx @@ -1,8 +1,8 @@ -import { useEffect } from "react"; -import { Controller, useForm } from "react-hook-form"; import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -19,7 +19,7 @@ import { Tooltip } from "@app/components/v2"; import { envConfig } from "@app/config/env"; -import { useProject } from "@app/context"; +import { useProject, useSubscription } from "@app/context"; import { useListCasByProjectId } from "@app/hooks/api/ca/queries"; import { TCertificateProfileWithDetails, @@ -29,6 +29,7 @@ import { useUpdateCertificateProfile } from "@app/hooks/api/certificateProfiles"; import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplates/queries"; +import { UsePopUpState } from "@app/hooks/usePopUp"; const createSchema = z .object({ @@ -151,12 +152,25 @@ export type FormData = z.infer; interface Props { isOpen: boolean; onClose: () => void; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["upgradePlan"]>, + data?: { + isEnterpriseFeature?: boolean; + } + ) => void; profile?: TCertificateProfileWithDetails; mode?: "create" | "edit"; } -export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }: Props) => { +export const CreateProfileModal = ({ + isOpen, + onClose, + handlePopUpOpen, + profile, + mode = "create" +}: Props) => { const { currentProject } = useProject(); + const { subscription } = useSubscription(); const { data: caData } = useListCasByProjectId(currentProject?.id || ""); const { data: templateData } = useListCertificateTemplatesV2({ @@ -248,6 +262,15 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" } }, [isEdit, profile, reset]); const onFormSubmit = async (data: FormData) => { + if (!isEdit && !subscription?.pkiAcme && data.enrollmentType === "acme") { + reset(); + onClose(); + handlePopUpOpen("upgradePlan", { + isEnterpriseFeature: true + }); + return; + } + if (!currentProject?.id && !isEdit) return; if (isEdit) { From 2767ff5c2d8792f0b1449e7cafbc18b2355b68b7 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 17:00:55 -0800 Subject: [PATCH 03/24] Remove unused stuff --- .../CertificateProfilesTab/CertificateProfilesTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx index c58a2fe20..79076fde8 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx @@ -31,7 +31,7 @@ export const CertificateProfilesTab = () => { const [selectedProfile, setSelectedProfile] = useState( null ); - const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ "enrollmentOptions", "upgradePlan" ] as const); From 33b1ee02477df546e8a5ed48584f5b5ad73f548e Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 17:16:33 -0800 Subject: [PATCH 04/24] Gate PKI feature # Conflicts: # backend/src/ee/services/pki-acme/pki-acme-service.ts --- .../src/ee/services/pki-acme/pki-acme-service.ts | 7 +++++++ .../certificate-profile/certificate-profile-dal.ts | 14 +++++++++++++- .../certificate-profile-types.ts | 4 ++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index fa0d0ff66..57d5c6ebd 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -29,6 +29,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getConfig } from "@app/lib/config/env"; +import { TLicenseServiceFactory } from "../license/license-service"; import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal"; import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal"; import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal"; @@ -101,6 +102,7 @@ type TPkiAcmeServiceFactoryDep = { >; keyStore: Pick; kmsService: Pick; + licenseService: Pick; certificateV3Service: Pick; acmeChallengeService: TPkiAcmeChallengeServiceFactory; }; @@ -116,6 +118,7 @@ export const pkiAcmeServiceFactory = ({ acmeChallengeDAL, keyStore, kmsService, + licenseService, certificateV3Service, acmeChallengeService }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { @@ -127,6 +130,10 @@ export const pkiAcmeServiceFactory = ({ if (profile.enrollmentType !== EnrollmentType.ACME) { throw new NotFoundError({ message: "Certificate profile is not configured for ACME enrollment" }); } + const orgLicensePlan = await licenseService.getPlan(profile.project.orgId); + if (!orgLicensePlan.pkiAcme) { + throw new AcmeUnauthorizedError({ message: "The organization does not have a valid license to use ACME" }); + } return profile; }; diff --git a/backend/src/services/certificate-profile/certificate-profile-dal.ts b/backend/src/services/certificate-profile/certificate-profile-dal.ts index 6415145ce..6eaea7c62 100644 --- a/backend/src/services/certificate-profile/certificate-profile-dal.ts +++ b/backend/src/services/certificate-profile/certificate-profile-dal.ts @@ -85,6 +85,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { const findByIdWithConfigs = async (id: string, tx?: Knex): Promise => { try { const query = (tx || db)(TableName.PkiCertificateProfile) + .leftJoin(TableName.Project, `${TableName.PkiCertificateProfile}.projectId`, `${TableName.Project}.id`) .leftJoin( TableName.CertificateAuthority, `${TableName.PkiCertificateProfile}.caId`, @@ -112,6 +113,8 @@ export const certificateProfileDALFactory = (db: TDbClient) => { ) .select(selectAllTableCols(TableName.PkiCertificateProfile)) .select( + db.ref("id").withSchema(TableName.Project).as("projectId"), + db.ref("orgId").withSchema(TableName.Project).as("orgId") db.ref("id").withSchema(TableName.CertificateAuthority).as("caId"), db.ref("projectId").withSchema(TableName.CertificateAuthority).as("caProjectId"), db.ref("status").withSchema(TableName.CertificateAuthority).as("caStatus"), @@ -131,7 +134,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenew"), db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigRenewBeforeDays"), db.ref("id").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeConfigId"), - db.ref("encryptedEabSecret").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeConfigEncryptedEabSecret") + db.ref("encryptedEabSecret").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeConfigEncryptedEabSecret"), ) .where(`${TableName.PkiCertificateProfile}.id`, id) .first(); @@ -185,6 +188,14 @@ export const certificateProfileDALFactory = (db: TDbClient) => { } as TCertificateProfileWithConfigs["certificateTemplate"]) : undefined; + const project = + result.projectId && result.orgId + ? ({ + id: result.projectId, + orgId: result.orgId + } as TCertificateProfileWithConfigs["project"]) + : undefined; + const transformedResult: TCertificateProfileWithConfigs = { id: result.id, projectId: result.projectId, @@ -201,6 +212,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { estConfig, apiConfig, acmeConfig, + project, certificateAuthority, certificateTemplate }; diff --git a/backend/src/services/certificate-profile/certificate-profile-types.ts b/backend/src/services/certificate-profile/certificate-profile-types.ts index 4a3339857..ebb93c63a 100644 --- a/backend/src/services/certificate-profile/certificate-profile-types.ts +++ b/backend/src/services/certificate-profile/certificate-profile-types.ts @@ -33,6 +33,10 @@ export type TCertificateProfileUpdate = Omit Date: Mon, 10 Nov 2025 17:16:45 -0800 Subject: [PATCH 05/24] Add missing stuff --- backend/src/server/routes/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 7c2e3b326..2d4537dd0 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -2238,6 +2238,7 @@ export const registerRoutes = async ( acmeChallengeDAL, keyStore, kmsService, + licenseService, certificateV3Service, acmeChallengeService }); From bb46eac8ea13bddd05b66b9e28c0fbb996204858 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 17:19:17 -0800 Subject: [PATCH 06/24] Syntax error --- .../services/certificate-profile/certificate-profile-dal.ts | 4 ++-- frontend/src/hooks/api/subscriptions/types.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/services/certificate-profile/certificate-profile-dal.ts b/backend/src/services/certificate-profile/certificate-profile-dal.ts index 6eaea7c62..cbde3ba69 100644 --- a/backend/src/services/certificate-profile/certificate-profile-dal.ts +++ b/backend/src/services/certificate-profile/certificate-profile-dal.ts @@ -114,7 +114,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { .select(selectAllTableCols(TableName.PkiCertificateProfile)) .select( db.ref("id").withSchema(TableName.Project).as("projectId"), - db.ref("orgId").withSchema(TableName.Project).as("orgId") + db.ref("orgId").withSchema(TableName.Project).as("orgId"), db.ref("id").withSchema(TableName.CertificateAuthority).as("caId"), db.ref("projectId").withSchema(TableName.CertificateAuthority).as("caProjectId"), db.ref("status").withSchema(TableName.CertificateAuthority).as("caStatus"), @@ -134,7 +134,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenew"), db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigRenewBeforeDays"), db.ref("id").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeConfigId"), - db.ref("encryptedEabSecret").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeConfigEncryptedEabSecret"), + db.ref("encryptedEabSecret").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeConfigEncryptedEabSecret") ) .where(`${TableName.PkiCertificateProfile}.id`, id) .first(); diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 80bea3db7..43ee68c3e 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -48,6 +48,7 @@ export type SubscriptionPlan = { gateway: boolean; externalKms: boolean; pkiEst: boolean; + pkiAcme: boolean; pkiLegacyTemplates: boolean; enforceMfa: boolean; enforceGoogleSSO: boolean; From 07cfb6dd54f78fb5c8e07e7a0593d9bb266df268 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 17:27:50 -0800 Subject: [PATCH 07/24] Check license when creating profile --- backend/src/server/routes/index.ts | 1 + .../certificate-profile-service.ts | 23 ++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 2d4537dd0..2bb6d0c2e 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1185,6 +1185,7 @@ export const registerRoutes = async ( certificateAuthorityCertDAL, permissionService, kmsService, + licenseService, projectDAL }); diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index 66a23a0e7..f629c817b 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -14,13 +14,14 @@ import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; 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 { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal"; +import { getCertificateCredentials, isCertChainValid } from "../certificate/certificate-fns"; +import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; 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"; @@ -153,6 +154,7 @@ type TCertificateProfileServiceFactoryDep = { certificateAuthorityCertDAL: Pick; permissionService: Pick; kmsService: Pick; + licenseService: Pick; projectDAL: Pick; }; @@ -175,6 +177,7 @@ export const certificateProfileServiceFactory = ({ certificateSecretDAL, permissionService, kmsService, + licenseService, projectDAL }: TCertificateProfileServiceFactoryDep) => { const createProfile = async ({ @@ -237,6 +240,20 @@ export const certificateProfileServiceFactory = ({ message: "API enrollment requires API configuration" }); } + if (data.enrollmentType === EnrollmentType.ACME) { + const orgLicensePlan = await licenseService.getPlan(actorOrgId); + if (!orgLicensePlan.pkiAcme) { + throw new BadRequestError({ + message: + "Failed to create certificate profile due to plan restriction. Upgrade plan to create ACME certificate profile." + }); + } + if (!data.acmeConfig) { + throw new ForbiddenRequestError({ + message: "ACME enrollment requires ACME configuration" + }); + } + } // TODO: acme type currently doesn't require config obj, but add a check in the future if // we have options From 8fcdce19aa9c7e49c38b8f1361a65426ee39bfb3 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 19:44:53 -0800 Subject: [PATCH 08/24] Fix type error --- .../certificate-profile/certificate-profile-dal.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/backend/src/services/certificate-profile/certificate-profile-dal.ts b/backend/src/services/certificate-profile/certificate-profile-dal.ts index cbde3ba69..53172b744 100644 --- a/backend/src/services/certificate-profile/certificate-profile-dal.ts +++ b/backend/src/services/certificate-profile/certificate-profile-dal.ts @@ -188,13 +188,10 @@ export const certificateProfileDALFactory = (db: TDbClient) => { } as TCertificateProfileWithConfigs["certificateTemplate"]) : undefined; - const project = - result.projectId && result.orgId - ? ({ - id: result.projectId, - orgId: result.orgId - } as TCertificateProfileWithConfigs["project"]) - : undefined; + const project = { + id: result.projectId, + orgId: result.orgId + } as TCertificateProfileWithConfigs["project"]; const transformedResult: TCertificateProfileWithConfigs = { id: result.id, From cb750c8c137aa8332572a3eb1f6ed62bea522f0d Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 19:58:37 -0800 Subject: [PATCH 09/24] Fix broken tests --- .../certificate-profile-service.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/backend/src/services/certificate-profile/certificate-profile-service.test.ts b/backend/src/services/certificate-profile/certificate-profile-service.test.ts index 9d9ab5947..7fa03276c 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.test.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.test.ts @@ -22,6 +22,7 @@ import type { TProjectDALFactory } from "../project/project-dal"; import type { TCertificateProfileDALFactory } from "./certificate-profile-dal"; import { certificateProfileServiceFactory, TCertificateProfileServiceFactory } from "./certificate-profile-service"; import { EnrollmentType, TCertificateProfile, TCertificateProfileWithConfigs } from "./certificate-profile-types"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; vi.mock("@app/lib/crypto/cryptography", () => ({ crypto: { @@ -99,6 +100,10 @@ describe("CertificateProfileService", () => { const sampleProfileWithConfigs: TCertificateProfileWithConfigs = { ...sampleProfile, + project: { + id: "project-123", + orgId: "org-123" + }, certificateAuthority: { id: "ca-123", projectId: "project-123", @@ -174,6 +179,12 @@ describe("CertificateProfileService", () => { generateKmsKey: vi.fn() } as unknown as Pick; + const mockLicenseService = { + getPlan: vi.fn().mockResolvedValue({ + pkiAcme: true + }) + } as unknown as Pick; + const mockProjectDAL = { findById: vi.fn(), findOne: vi.fn(), @@ -253,6 +264,7 @@ describe("CertificateProfileService", () => { certificateAuthorityCertDAL: mockCertificateAuthorityCertDAL, permissionService: mockPermissionService, kmsService: mockKmsService, + licenseService: mockLicenseService, projectDAL: mockProjectDAL }); }); From 192cf5a8f9e0d6dc26fe297be538ddf00f71cc4e Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 21:30:50 -0800 Subject: [PATCH 10/24] Revert stuff --- .../certificate-profile-dal.ts | 9 -------- .../certificate-profile-service.test.ts | 12 ---------- .../certificate-profile-service.ts | 23 +++---------------- .../certificate-profile-types.ts | 4 ---- 4 files changed, 3 insertions(+), 45 deletions(-) diff --git a/backend/src/services/certificate-profile/certificate-profile-dal.ts b/backend/src/services/certificate-profile/certificate-profile-dal.ts index 53172b744..6415145ce 100644 --- a/backend/src/services/certificate-profile/certificate-profile-dal.ts +++ b/backend/src/services/certificate-profile/certificate-profile-dal.ts @@ -85,7 +85,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => { const findByIdWithConfigs = async (id: string, tx?: Knex): Promise => { try { const query = (tx || db)(TableName.PkiCertificateProfile) - .leftJoin(TableName.Project, `${TableName.PkiCertificateProfile}.projectId`, `${TableName.Project}.id`) .leftJoin( TableName.CertificateAuthority, `${TableName.PkiCertificateProfile}.caId`, @@ -113,8 +112,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => { ) .select(selectAllTableCols(TableName.PkiCertificateProfile)) .select( - db.ref("id").withSchema(TableName.Project).as("projectId"), - db.ref("orgId").withSchema(TableName.Project).as("orgId"), db.ref("id").withSchema(TableName.CertificateAuthority).as("caId"), db.ref("projectId").withSchema(TableName.CertificateAuthority).as("caProjectId"), db.ref("status").withSchema(TableName.CertificateAuthority).as("caStatus"), @@ -188,11 +185,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => { } as TCertificateProfileWithConfigs["certificateTemplate"]) : undefined; - const project = { - id: result.projectId, - orgId: result.orgId - } as TCertificateProfileWithConfigs["project"]; - const transformedResult: TCertificateProfileWithConfigs = { id: result.id, projectId: result.projectId, @@ -209,7 +201,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => { estConfig, apiConfig, acmeConfig, - project, certificateAuthority, certificateTemplate }; diff --git a/backend/src/services/certificate-profile/certificate-profile-service.test.ts b/backend/src/services/certificate-profile/certificate-profile-service.test.ts index 7fa03276c..9d9ab5947 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.test.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.test.ts @@ -22,7 +22,6 @@ import type { TProjectDALFactory } from "../project/project-dal"; import type { TCertificateProfileDALFactory } from "./certificate-profile-dal"; import { certificateProfileServiceFactory, TCertificateProfileServiceFactory } from "./certificate-profile-service"; import { EnrollmentType, TCertificateProfile, TCertificateProfileWithConfigs } from "./certificate-profile-types"; -import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; vi.mock("@app/lib/crypto/cryptography", () => ({ crypto: { @@ -100,10 +99,6 @@ describe("CertificateProfileService", () => { const sampleProfileWithConfigs: TCertificateProfileWithConfigs = { ...sampleProfile, - project: { - id: "project-123", - orgId: "org-123" - }, certificateAuthority: { id: "ca-123", projectId: "project-123", @@ -179,12 +174,6 @@ describe("CertificateProfileService", () => { generateKmsKey: vi.fn() } as unknown as Pick; - const mockLicenseService = { - getPlan: vi.fn().mockResolvedValue({ - pkiAcme: true - }) - } as unknown as Pick; - const mockProjectDAL = { findById: vi.fn(), findOne: vi.fn(), @@ -264,7 +253,6 @@ describe("CertificateProfileService", () => { certificateAuthorityCertDAL: mockCertificateAuthorityCertDAL, permissionService: mockPermissionService, kmsService: mockKmsService, - licenseService: mockLicenseService, projectDAL: mockProjectDAL }); }); diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index f629c817b..66a23a0e7 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -14,14 +14,13 @@ import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; -import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; -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 { 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 { 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"; @@ -154,7 +153,6 @@ type TCertificateProfileServiceFactoryDep = { certificateAuthorityCertDAL: Pick; permissionService: Pick; kmsService: Pick; - licenseService: Pick; projectDAL: Pick; }; @@ -177,7 +175,6 @@ export const certificateProfileServiceFactory = ({ certificateSecretDAL, permissionService, kmsService, - licenseService, projectDAL }: TCertificateProfileServiceFactoryDep) => { const createProfile = async ({ @@ -240,20 +237,6 @@ export const certificateProfileServiceFactory = ({ message: "API enrollment requires API configuration" }); } - if (data.enrollmentType === EnrollmentType.ACME) { - const orgLicensePlan = await licenseService.getPlan(actorOrgId); - if (!orgLicensePlan.pkiAcme) { - throw new BadRequestError({ - message: - "Failed to create certificate profile due to plan restriction. Upgrade plan to create ACME certificate profile." - }); - } - if (!data.acmeConfig) { - throw new ForbiddenRequestError({ - message: "ACME enrollment requires ACME configuration" - }); - } - } // TODO: acme type currently doesn't require config obj, but add a check in the future if // we have options diff --git a/backend/src/services/certificate-profile/certificate-profile-types.ts b/backend/src/services/certificate-profile/certificate-profile-types.ts index ebb93c63a..4a3339857 100644 --- a/backend/src/services/certificate-profile/certificate-profile-types.ts +++ b/backend/src/services/certificate-profile/certificate-profile-types.ts @@ -33,10 +33,6 @@ export type TCertificateProfileUpdate = Omit Date: Mon, 10 Nov 2025 21:36:08 -0800 Subject: [PATCH 11/24] Bring changes back --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 2 +- .../certificate-profile/certificate-profile-dal.ts | 9 +++++++++ .../certificate-profile/certificate-profile-types.ts | 4 ++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 57d5c6ebd..a0dbd65b4 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -130,7 +130,7 @@ export const pkiAcmeServiceFactory = ({ if (profile.enrollmentType !== EnrollmentType.ACME) { throw new NotFoundError({ message: "Certificate profile is not configured for ACME enrollment" }); } - const orgLicensePlan = await licenseService.getPlan(profile.project.orgId); + const orgLicensePlan = await licenseService.getPlan(profile.project!.orgId); if (!orgLicensePlan.pkiAcme) { throw new AcmeUnauthorizedError({ message: "The organization does not have a valid license to use ACME" }); } diff --git a/backend/src/services/certificate-profile/certificate-profile-dal.ts b/backend/src/services/certificate-profile/certificate-profile-dal.ts index 6415145ce..53172b744 100644 --- a/backend/src/services/certificate-profile/certificate-profile-dal.ts +++ b/backend/src/services/certificate-profile/certificate-profile-dal.ts @@ -85,6 +85,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { const findByIdWithConfigs = async (id: string, tx?: Knex): Promise => { try { const query = (tx || db)(TableName.PkiCertificateProfile) + .leftJoin(TableName.Project, `${TableName.PkiCertificateProfile}.projectId`, `${TableName.Project}.id`) .leftJoin( TableName.CertificateAuthority, `${TableName.PkiCertificateProfile}.caId`, @@ -112,6 +113,8 @@ export const certificateProfileDALFactory = (db: TDbClient) => { ) .select(selectAllTableCols(TableName.PkiCertificateProfile)) .select( + db.ref("id").withSchema(TableName.Project).as("projectId"), + db.ref("orgId").withSchema(TableName.Project).as("orgId"), db.ref("id").withSchema(TableName.CertificateAuthority).as("caId"), db.ref("projectId").withSchema(TableName.CertificateAuthority).as("caProjectId"), db.ref("status").withSchema(TableName.CertificateAuthority).as("caStatus"), @@ -185,6 +188,11 @@ export const certificateProfileDALFactory = (db: TDbClient) => { } as TCertificateProfileWithConfigs["certificateTemplate"]) : undefined; + const project = { + id: result.projectId, + orgId: result.orgId + } as TCertificateProfileWithConfigs["project"]; + const transformedResult: TCertificateProfileWithConfigs = { id: result.id, projectId: result.projectId, @@ -201,6 +209,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { estConfig, apiConfig, acmeConfig, + project, certificateAuthority, certificateTemplate }; diff --git a/backend/src/services/certificate-profile/certificate-profile-types.ts b/backend/src/services/certificate-profile/certificate-profile-types.ts index 4a3339857..030548e97 100644 --- a/backend/src/services/certificate-profile/certificate-profile-types.ts +++ b/backend/src/services/certificate-profile/certificate-profile-types.ts @@ -33,6 +33,10 @@ export type TCertificateProfileUpdate = Omit Date: Tue, 11 Nov 2025 09:17:08 -0800 Subject: [PATCH 12/24] Remove ACME global feature flag --- backend/src/ee/routes/v1/index.ts | 5 +---- backend/src/lib/config/env.ts | 5 ----- backend/src/server/plugins/serve-ui.ts | 5 +---- frontend/src/global.d.ts | 1 - .../components/CertificateProfilesTab/CreateProfileModal.tsx | 3 +-- 5 files changed, 3 insertions(+), 16 deletions(-) diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index ce05ea3b6..7061fd269 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -109,10 +109,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { await server.register( async (pkiRouter) => { await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" }); - // Notice: current this feature is still in development and is not yet ready for production. - if (getConfig().isAcmeFeatureEnabled === true) { - await pkiRouter.register(registerPkiAcmeRouter, { prefix: "/acme" }); - } + await pkiRouter.register(registerPkiAcmeRouter, { prefix: "/acme" }); }, { prefix: "/pki" } ); diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index b60971b1f..6f0502184 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -106,10 +106,6 @@ const envSchema = z HTTPS_ENABLED: zodStrBool, ROTATION_DEVELOPMENT_MODE: zodStrBool.default("false").optional(), DAILY_RESOURCE_CLEAN_UP_DEVELOPMENT_MODE: zodStrBool.default("false").optional(), - // Note: The ACME feature is still in development and is not yet ready for production. - // This is the feature flag to enable/disable the ACME feature. - // It's not intended to be used by users outside of the development team yet. - ACME_FEATURE_ENABLED: zodStrBool.default("false").optional(), ACME_DEVELOPMENT_MODE: zodStrBool.default("false").optional(), ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES: zpStr( z @@ -399,7 +395,6 @@ const envSchema = z (data.NODE_ENV === "development" && data.ROTATION_DEVELOPMENT_MODE) || data.NODE_ENV === "test", isDailyResourceCleanUpDevelopmentMode: data.NODE_ENV === "development" && data.DAILY_RESOURCE_CLEAN_UP_DEVELOPMENT_MODE, - isAcmeFeatureEnabled: data.NODE_ENV === "development" && data.ACME_FEATURE_ENABLED === true, isAcmeDevelopmentMode: data.NODE_ENV === "development" && data.ACME_DEVELOPMENT_MODE, isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED, isRedisSentinelMode: Boolean(data.REDIS_SENTINEL_HOSTS), diff --git a/backend/src/server/plugins/serve-ui.ts b/backend/src/server/plugins/serve-ui.ts index 4330f9397..b71451b6e 100644 --- a/backend/src/server/plugins/serve-ui.ts +++ b/backend/src/server/plugins/serve-ui.ts @@ -31,10 +31,7 @@ export const registerServeUI = async ( CAPTCHA_SITE_KEY: appCfg.CAPTCHA_SITE_KEY, POSTHOG_API_KEY: appCfg.POSTHOG_PROJECT_API_KEY, INTERCOM_ID: appCfg.INTERCOM_ID, - TELEMETRY_CAPTURING_ENABLED: appCfg.TELEMETRY_ENABLED, - // The feature flag to enable/disable the ACME feature. - // Will be removed once the feature is ready for production. - ACME_FEATURE_ENABLED: appCfg.isAcmeFeatureEnabled + TELEMETRY_CAPTURING_ENABLED: appCfg.TELEMETRY_ENABLED }; const js = `window.__INFISICAL_RUNTIME_ENV__ = Object.freeze(${JSON.stringify(config)});`; return res.send(js); diff --git a/frontend/src/global.d.ts b/frontend/src/global.d.ts index a6de7ac8c..30b80cb70 100644 --- a/frontend/src/global.d.ts +++ b/frontend/src/global.d.ts @@ -7,7 +7,6 @@ declare global { POSTHOG_API_KEY?: string; INTERCOM_ID?: string; TELEMETRY_CAPTURING_ENABLED: string; - ACME_FEATURE_ENABLED?: boolean; }; } } diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx index 3214b1bb1..712455ce9 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx @@ -18,7 +18,6 @@ import { TextArea, Tooltip } from "@app/components/v2"; -import { envConfig } from "@app/config/env"; import { useProject, useSubscription } from "@app/context"; import { useListCasByProjectId } from "@app/hooks/api/ca/queries"; import { @@ -490,7 +489,7 @@ export const CreateProfileModal = ({ > API EST - {envConfig.ACME_FEATURE_ENABLED && ACME} + ACME )} From c3d5169f63376d5bef0e7ba8e82da1c3842e17f6 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 11 Nov 2025 09:18:06 -0800 Subject: [PATCH 13/24] Remove env --- frontend/src/config/env.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/config/env.ts b/frontend/src/config/env.ts index c8100bd16..63446c95f 100644 --- a/frontend/src/config/env.ts +++ b/frontend/src/config/env.ts @@ -26,9 +26,6 @@ export const envConfig = { import.meta.env.VITE_TELEMETRY_CAPTURING_ENABLED === true ); }, - get ACME_FEATURE_ENABLED() { - return window?.__INFISICAL_RUNTIME_ENV__?.ACME_FEATURE_ENABLED ?? false; - }, get PLATFORM_VERSION() { return import.meta.env.VITE_INFISICAL_PLATFORM_VERSION; From 40be1ac9d90ecaeea488ac2c2bd2a963ab4bb92b Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 11 Nov 2025 09:24:10 -0800 Subject: [PATCH 14/24] Remove unused stuff --- .../CertificateProfilesTab/CertificateProfilesTab.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx index 79076fde8..3218187c4 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx @@ -31,10 +31,7 @@ export const CertificateProfilesTab = () => { const [selectedProfile, setSelectedProfile] = useState( null ); - const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ - "enrollmentOptions", - "upgradePlan" - ] as const); + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const); const deleteProfile = useDeleteCertificateProfile(); From c687cbc13112968da05911979261e6feb561d57e Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 11 Nov 2025 10:12:54 -0800 Subject: [PATCH 15/24] Set pki acme flag type as false to avoid committing them to true by accident --- backend/src/ee/services/license/license-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 659067957..5157b0730 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -78,7 +78,7 @@ export type TFeatureSet = { secretsLimit: number; }; pkiEst: boolean; - pkiAcme: boolean; + pkiAcme: false; enforceMfa: boolean; projectTemplates: false; kmip: false; From ff77130c654469094f4d3b048bef97a60ae31475 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 11 Nov 2025 10:37:46 -0800 Subject: [PATCH 16/24] Add missing changes --- backend/src/server/routes/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 2bb6d0c2e..9c8686d81 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -2230,6 +2230,7 @@ export const registerRoutes = async ( }); const pkiAcmeService = pkiAcmeServiceFactory({ projectDAL, + certificateAuthorityDAL, certificateProfileDAL, certificateBodyDAL, acmeAccountDAL, From 8efc245268721b95395d4ce8f262950343d8fa0c Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 11 Nov 2025 10:38:19 -0800 Subject: [PATCH 17/24] Revert "Add missing changes" This reverts commit be8683fec1484edc182abf0353d5f3a23a81d37f. --- backend/src/server/routes/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 9c8686d81..2bb6d0c2e 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -2230,7 +2230,6 @@ export const registerRoutes = async ( }); const pkiAcmeService = pkiAcmeServiceFactory({ projectDAL, - certificateAuthorityDAL, certificateProfileDAL, certificateBodyDAL, acmeAccountDAL, From 49370399acf4882ebbe8eed60a4a0b4c8aa83395 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 11 Nov 2025 10:39:11 -0800 Subject: [PATCH 18/24] Revert not needed changes --- backend/src/server/routes/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 2bb6d0c2e..2d4537dd0 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1185,7 +1185,6 @@ export const registerRoutes = async ( certificateAuthorityCertDAL, permissionService, kmsService, - licenseService, projectDAL }); From 1ef6ae440c55787bd5730cf125f84d324686a86f Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 11 Nov 2025 10:57:02 -0800 Subject: [PATCH 19/24] Remove unused stuff --- backend/src/ee/routes/v1/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 7061fd269..7ff9ec09a 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -1,5 +1,4 @@ import { registerProjectTemplateRouter } from "@app/ee/routes/v1/project-template-router"; -import { getConfig } from "@app/lib/config/env"; import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router"; import { registerAccessApprovalRequestRouter } from "./access-approval-request-router"; From c1938e67876af63a1ae54312483a1c061ad626f3 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 12 Nov 2025 10:55:37 -0800 Subject: [PATCH 20/24] Try to fix bdd tests with flag --- .github/workflows/run-backend-bdd-tests.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 52330582e..bf2075864 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -45,12 +45,13 @@ jobs: run: npm install working-directory: backend - - name: Output .env file + - name: Output .env file and enable feature flags for BDD tests run: | cp .env.example .env echo "ACME_DEVELOPMENT_MODE=true" >> .env - echo "ACME_FEATURE_ENABLED=true" >> .env echo "ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES={\"localhost\": \"host.docker.internal:8087\"}" >> .env + # Enable ACME feature in license for BDD tests + sed -i 's/pkiAcme: .*/pkiAcme: true,/g' backend/src/ee/services/license/license-fns.ts - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: From 45941b4cc8ea5c787684096c191994237e11d2ed Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 12 Nov 2025 10:59:42 -0800 Subject: [PATCH 21/24] Fix import issue --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index a0dbd65b4..0521c2a6d 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -43,6 +43,7 @@ import { AcmeMalformedError, AcmeOrderNotReadyError, AcmeServerInternalError, + AcmeUnauthorizedError, AcmeUnsupportedIdentifierError } from "./pki-acme-errors"; import { buildUrl, extractAccountIdFromKid, validateDnsIdentifier } from "./pki-acme-fns"; From a26b13e517eca34fe6ead1a9c704435614587f66 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 12 Nov 2025 11:42:18 -0800 Subject: [PATCH 22/24] Change error msg --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 0521c2a6d..55e9cc43f 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -133,7 +133,9 @@ export const pkiAcmeServiceFactory = ({ } const orgLicensePlan = await licenseService.getPlan(profile.project!.orgId); if (!orgLicensePlan.pkiAcme) { - throw new AcmeUnauthorizedError({ message: "The organization does not have a valid license to use ACME" }); + throw new AcmeUnauthorizedError({ + message: "Failed to validate ACME profile: Plan restriction. Upgrade plan to continue" + }); } return profile; }; From a424954d7e67c92be453ffbdcfcd91ee9d732145 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 12 Nov 2025 11:53:32 -0800 Subject: [PATCH 23/24] Check profile creation as well --- .../certificate-profile-service.test.ts | 44 +++++++++++++++++-- .../certificate-profile-service.ts | 20 +++++++-- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/backend/src/services/certificate-profile/certificate-profile-service.test.ts b/backend/src/services/certificate-profile/certificate-profile-service.test.ts index 9d9ab5947..8865ffb9b 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.test.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.test.ts @@ -6,14 +6,15 @@ import { ForbiddenError } from "@casl/ability"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; -import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { ActorType, AuthMethod } from "../auth/auth-type"; -import type { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal"; -import type { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; import type { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal"; import type { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; import type { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal"; +import type { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal"; +import type { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; import { TAcmeEnrollmentConfigDALFactory } from "../enrollment-config/acme-enrollment-config-dal"; import type { TApiEnrollmentConfigDALFactory } from "../enrollment-config/api-enrollment-config-dal"; import type { TEstEnrollmentConfigDALFactory } from "../enrollment-config/est-enrollment-config-dal"; @@ -166,6 +167,10 @@ describe("CertificateProfileService", () => { }) } as unknown as Pick; + const mockLicenseService = { + getPlan: vi.fn() + } as unknown as Pick; + const mockKmsService = { encryptWithKmsKey: vi .fn() @@ -252,6 +257,7 @@ describe("CertificateProfileService", () => { certificateAuthorityDAL: mockCertificateAuthorityDAL, certificateAuthorityCertDAL: mockCertificateAuthorityCertDAL, permissionService: mockPermissionService, + licenseService: mockLicenseService, kmsService: mockKmsService, projectDAL: mockProjectDAL }); @@ -275,6 +281,13 @@ describe("CertificateProfileService", () => { }; beforeEach(() => { + (mockProjectDAL.findById as any).mockResolvedValue({ + id: "project-123", + orgId: "org-123" + }); + (mockLicenseService.getPlan as any).mockResolvedValue({ + pkiAcme: true + }); (mockCertificateTemplateV2DAL.findById as any).mockResolvedValue(sampleTemplate); (mockCertificateProfileDAL.findByNameAndProjectId as any).mockResolvedValue(null); (mockCertificateProfileDAL.findBySlugAndProjectId as any).mockResolvedValue(null); @@ -405,6 +418,24 @@ describe("CertificateProfileService", () => { expect(result).toEqual(sampleProfile); expect(mockCertificateTemplateV2DAL.findById).toHaveBeenCalledWith("template-123"); }); + + it("should throw BadRequestError when plan does not support ACME", async () => { + (mockLicenseService.getPlan as any).mockResolvedValue({ + pkiAcme: false + }); + + await expect( + service.createProfile({ + ...mockActor, + projectId: "project-123", + data: validProfileData + }) + ).rejects.toThrowError( + new BadRequestError({ + message: "Failed to create certificate profile: Plan restriction. Upgrade plan to continue" + }) + ); + }); }); describe("updateProfile", () => { @@ -699,6 +730,13 @@ describe("CertificateProfileService", () => { } }; + (mockProjectDAL.findById as any).mockResolvedValue({ + id: "project-123", + orgId: "org-123" + }); + (mockLicenseService.getPlan as any).mockResolvedValue({ + pkiAcme: true + }); (mockCertificateTemplateV2DAL.findById as any).mockResolvedValue(sampleTemplate); (mockCertificateProfileDAL.findByNameAndProjectId as any).mockResolvedValue(null); (mockCertificateProfileDAL.findBySlugAndProjectId as any).mockResolvedValue(null); diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index 66a23a0e7..fe58f958f 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -14,13 +14,14 @@ import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; 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 { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal"; +import { getCertificateCredentials, isCertChainValid } from "../certificate/certificate-fns"; +import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; 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"; @@ -152,6 +153,7 @@ type TCertificateProfileServiceFactoryDep = { certificateAuthorityDAL: Pick; certificateAuthorityCertDAL: Pick; permissionService: Pick; + licenseService: Pick; kmsService: Pick; projectDAL: Pick; }; @@ -174,6 +176,7 @@ export const certificateProfileServiceFactory = ({ certificateBodyDAL, certificateSecretDAL, permissionService, + licenseService, kmsService, projectDAL }: TCertificateProfileServiceFactoryDep) => { @@ -205,6 +208,17 @@ export const certificateProfileServiceFactory = ({ ProjectPermissionSub.CertificateProfiles ); + const project = await projectDAL.findById(projectId); + if (!project) { + throw new NotFoundError({ message: "Project not found" }); + } + const plan = await licenseService.getPlan(project.orgId); + if (!plan.pkiAcme) { + throw new BadRequestError({ + message: "Failed to create certificate profile: Plan restriction. Upgrade plan to continue" + }); + } + // Validate that certificate template exists and belongs to the same project if (data.certificateTemplateId) { const template = await certificateTemplateV2DAL.findById(data.certificateTemplateId); From 2b3418c85fbdd2f9c71d581b3e89ba685619fbe1 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 12 Nov 2025 11:57:38 -0800 Subject: [PATCH 24/24] Add missing license server --- backend/src/server/routes/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 2d4537dd0..067d296a3 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1184,6 +1184,7 @@ export const registerRoutes = async ( certificateAuthorityDAL, certificateAuthorityCertDAL, permissionService, + licenseService, kmsService, projectDAL });