diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index f181d1c23..49a11691c 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -7,6 +7,7 @@ import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-se import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service"; import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service"; +import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service"; import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service"; import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service"; import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service"; @@ -36,7 +37,6 @@ import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service"; import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; -import { TCertificateEstServiceFactory } from "@app/services/certificate-est/certificate-est-service"; import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; diff --git a/backend/src/server/routes/est/certificate-est-router.ts b/backend/src/ee/routes/est/certificate-est-router.ts similarity index 100% rename from backend/src/server/routes/est/certificate-est-router.ts rename to backend/src/ee/routes/est/certificate-est-router.ts diff --git a/backend/src/services/certificate-est/certificate-est-fns.ts b/backend/src/ee/services/certificate-est/certificate-est-fns.ts similarity index 100% rename from backend/src/services/certificate-est/certificate-est-fns.ts rename to backend/src/ee/services/certificate-est/certificate-est-fns.ts diff --git a/backend/src/services/certificate-est/certificate-est-service.ts b/backend/src/ee/services/certificate-est/certificate-est-service.ts similarity index 77% rename from backend/src/services/certificate-est/certificate-est-service.ts rename to backend/src/ee/services/certificate-est/certificate-est-service.ts index e85d4658b..b8e179856 100644 --- a/backend/src/services/certificate-est/certificate-est-service.ts +++ b/backend/src/ee/services/certificate-est/certificate-est-service.ts @@ -1,16 +1,17 @@ import * as x509 from "@peculiar/x509"; import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { isCertChainValid } from "@app/services/certificate/certificate-fns"; +import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; +import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; +import { getCaCertChain, getCaCertChains } from "@app/services/certificate-authority/certificate-authority-fns"; +import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; +import { TCertificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal"; +import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; -import { isCertChainValid } from "../certificate/certificate-fns"; -import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal"; -import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; -import { getCaCertChain, getCaCertChains } from "../certificate-authority/certificate-authority-fns"; -import { TCertificateAuthorityServiceFactory } from "../certificate-authority/certificate-authority-service"; -import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal"; -import { TCertificateTemplateServiceFactory } from "../certificate-template/certificate-template-service"; -import { TKmsServiceFactory } from "../kms/kms-service"; -import { TProjectDALFactory } from "../project/project-dal"; +import { TLicenseServiceFactory } from "../license/license-service"; import { convertRawCertsToPkcs7 } from "./certificate-est-fns"; type TCertificateEstServiceFactoryDep = { @@ -21,6 +22,7 @@ type TCertificateEstServiceFactoryDep = { certificateAuthorityCertDAL: Pick; projectDAL: Pick; kmsService: Pick; + licenseService: Pick; }; export type TCertificateEstServiceFactory = ReturnType; @@ -32,7 +34,8 @@ export const certificateEstServiceFactory = ({ certificateAuthorityCertDAL, certificateAuthorityDAL, projectDAL, - kmsService + kmsService, + licenseService }: TCertificateEstServiceFactoryDep) => { const simpleReenroll = async ({ csr, @@ -48,6 +51,14 @@ export const certificateEstServiceFactory = ({ certificateTemplateId }); + const plan = await licenseService.getPlan(estConfig.orgId); + if (!plan.pkiEst) { + throw new BadRequestError({ + message: + "Failed to perform EST operation - simpleReenroll due to plan restriction. Upgrade to the Enterprise plan." + }); + } + if (!estConfig.isEnabled) { throw new BadRequestError({ message: "EST is disabled" @@ -146,6 +157,14 @@ export const certificateEstServiceFactory = ({ certificateTemplateId }); + const plan = await licenseService.getPlan(estConfig.orgId); + if (!plan.pkiEst) { + throw new BadRequestError({ + message: + "Failed to perform EST operation - simpleEnroll due to plan restriction. Upgrade to the Enterprise plan." + }); + } + if (!estConfig.isEnabled) { throw new BadRequestError({ message: "EST is disabled" @@ -196,6 +215,24 @@ export const certificateEstServiceFactory = ({ }); } + const estConfig = await certificateTemplateService.getEstConfiguration({ + isInternal: true, + certificateTemplateId + }); + + const plan = await licenseService.getPlan(estConfig.orgId); + if (!plan.pkiEst) { + throw new BadRequestError({ + message: "Failed to perform EST operation - caCerts due to plan restriction. Upgrade to the Enterprise plan." + }); + } + + if (!estConfig.isEnabled) { + throw new BadRequestError({ + message: "EST is disabled" + }); + } + const ca = await certificateAuthorityDAL.findById(certTemplate.caId); if (!ca) { throw new NotFoundError({ diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index bd40f75cb..beb981a5a 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -45,7 +45,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ readLimit: 60, writeLimit: 200, secretsLimit: 40 - } + }, + pkiEst: false }); export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 70db8e4d9..6e671c26f 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -63,6 +63,7 @@ export type TFeatureSet = { writeLimit: number; secretsLimit: number; }; + pkiEst: boolean; }; export type TOrgPlansTableDTO = { diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 349a41754..f33456bd5 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -3,6 +3,7 @@ import { Redis } from "ioredis"; import { Knex } from "knex"; import { z } from "zod"; +import { registerCertificateEstRouter } from "@app/ee/routes/est/certificate-est-router"; import { registerV1EERoutes } from "@app/ee/routes/v1"; import { accessApprovalPolicyApproverDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal"; import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal"; @@ -17,6 +18,7 @@ import { auditLogStreamDALFactory } from "@app/ee/services/audit-log-stream/audi import { auditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service"; import { certificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { certificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service"; +import { certificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service"; import { dynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal"; import { dynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service"; import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/providers"; @@ -92,7 +94,6 @@ import { certificateAuthorityDALFactory } from "@app/services/certificate-author import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue"; import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal"; import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; -import { certificateEstServiceFactory } from "@app/services/certificate-est/certificate-est-service"; import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal"; import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal"; import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; @@ -199,7 +200,6 @@ import { injectIdentity } from "../plugins/auth/inject-identity"; import { injectPermission } from "../plugins/auth/inject-permission"; import { injectRateLimits } from "../plugins/inject-rate-limits"; import { registerSecretScannerGhApp } from "../plugins/secret-scanner"; -import { registerCertificateEstRouter } from "./est/certificate-est-router"; import { registerV1Routes } from "./v1"; import { registerV2Routes } from "./v2"; import { registerV3Routes } from "./v3"; @@ -667,7 +667,8 @@ export const registerRoutes = async ( certificateAuthorityDAL, permissionService, kmsService, - projectDAL + projectDAL, + licenseService }); const certificateEstService = certificateEstServiceFactory({ @@ -677,7 +678,8 @@ export const registerRoutes = async ( certificateAuthorityCertDAL, certificateAuthorityDAL, projectDAL, - kmsService + kmsService, + licenseService }); const pkiAlertService = pkiAlertServiceFactory({ diff --git a/backend/src/services/certificate-template/certificate-template-dal.ts b/backend/src/services/certificate-template/certificate-template-dal.ts index a30c4890d..c500833d1 100644 --- a/backend/src/services/certificate-template/certificate-template-dal.ts +++ b/backend/src/services/certificate-template/certificate-template-dal.ts @@ -1,3 +1,5 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; @@ -30,20 +32,21 @@ export const certificateTemplateDALFactory = (db: TDbClient) => { } }; - const getById = async (id: string) => { + const getById = async (id: string, tx?: Knex) => { try { - const certTemplate = await db - .replicaNode()(TableName.CertificateTemplate) + const certTemplate = await (tx || db.replicaNode())(TableName.CertificateTemplate) .join( TableName.CertificateAuthority, `${TableName.CertificateAuthority}.id`, `${TableName.CertificateTemplate}.caId` ) + .join(TableName.Project, `${TableName.Project}.id`, `${TableName.CertificateAuthority}.projectId`) .where(`${TableName.CertificateTemplate}.id`, "=", id) .select(selectAllTableCols(TableName.CertificateTemplate)) .select( db.ref("projectId").withSchema(TableName.CertificateAuthority), - db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority) + db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority), + db.ref("orgId").withSchema(TableName.Project) ) .first(); diff --git a/backend/src/services/certificate-template/certificate-template-service.ts b/backend/src/services/certificate-template/certificate-template-service.ts index 31745e752..2a8134093 100644 --- a/backend/src/services/certificate-template/certificate-template-service.ts +++ b/backend/src/services/certificate-template/certificate-template-service.ts @@ -3,6 +3,7 @@ import * as x509 from "@peculiar/x509"; import bcrypt from "bcrypt"; import { TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; @@ -32,6 +33,7 @@ type TCertificateTemplateServiceFactoryDep = { kmsService: Pick; certificateAuthorityDAL: Pick; permissionService: Pick; + licenseService: Pick; }; export type TCertificateTemplateServiceFactory = ReturnType; @@ -42,7 +44,8 @@ export const certificateTemplateServiceFactory = ({ certificateAuthorityDAL, permissionService, kmsService, - projectDAL + projectDAL, + licenseService }: TCertificateTemplateServiceFactoryDep) => { const createCertTemplate = async ({ caId, @@ -75,23 +78,28 @@ export const certificateTemplateServiceFactory = ({ ProjectPermissionSub.CertificateTemplates ); - const { id } = await certificateTemplateDAL.create({ - caId, - pkiCollectionId, - name, - commonName, - subjectAlternativeName, - ttl + return certificateTemplateDAL.transaction(async (tx) => { + const { id } = await certificateTemplateDAL.create( + { + caId, + pkiCollectionId, + name, + commonName, + subjectAlternativeName, + ttl + }, + tx + ); + + const certificateTemplate = await certificateTemplateDAL.getById(id, tx); + if (!certificateTemplate) { + throw new NotFoundError({ + message: "Certificate template not found" + }); + } + + return certificateTemplate; }); - - const certificateTemplate = await certificateTemplateDAL.getById(id); - if (!certificateTemplate) { - throw new NotFoundError({ - message: "Certificate template not found" - }); - } - - return certificateTemplate; }; const updateCertTemplate = async ({ @@ -136,23 +144,29 @@ export const certificateTemplateServiceFactory = ({ } } - await certificateTemplateDAL.updateById(certTemplate.id, { - caId, - pkiCollectionId, - commonName, - subjectAlternativeName, - name, - ttl + return certificateTemplateDAL.transaction(async (tx) => { + await certificateTemplateDAL.updateById( + certTemplate.id, + { + caId, + pkiCollectionId, + commonName, + subjectAlternativeName, + name, + ttl + }, + tx + ); + + const updatedTemplate = await certificateTemplateDAL.getById(id, tx); + if (!updatedTemplate) { + throw new NotFoundError({ + message: "Certificate template not found" + }); + } + + return updatedTemplate; }); - - const updatedTemplate = await certificateTemplateDAL.getById(id); - if (!updatedTemplate) { - throw new NotFoundError({ - message: "Certificate template not found" - }); - } - - return updatedTemplate; }; const deleteCertTemplate = async ({ id, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertTemplateDTO) => { @@ -215,6 +229,13 @@ export const certificateTemplateServiceFactory = ({ actor, actorOrgId }: TCreateEstConfigurationDTO) => { + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.pkiEst) { + throw new BadRequestError({ + message: "Failed to create EST configuration due to plan restriction. Upgrade to the Enterprise plan." + }); + } + const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId); if (!certTemplate) { throw new NotFoundError({ @@ -285,6 +306,13 @@ export const certificateTemplateServiceFactory = ({ actor, actorOrgId }: TUpdateEstConfigurationDTO) => { + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.pkiEst) { + throw new BadRequestError({ + message: "Failed to update EST configuration due to plan restriction. Upgrade to the Enterprise plan." + }); + } + const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId); if (!certTemplate) { throw new NotFoundError({ @@ -416,7 +444,8 @@ export const certificateTemplateServiceFactory = ({ isEnabled: estConfig.isEnabled, caChain: decryptedCaChain.toString(), hashedPassphrase: estConfig.hashedPassphrase, - projectId: certTemplate.projectId + projectId: certTemplate.projectId, + orgId: certTemplate.orgId }; }; diff --git a/docs/documentation/platform/pki/est.mdx b/docs/documentation/platform/pki/est.mdx index aab822dd5..ee66c32b6 100644 --- a/docs/documentation/platform/pki/est.mdx +++ b/docs/documentation/platform/pki/est.mdx @@ -21,7 +21,8 @@ These endpoints are exposed on port 8443 under the .well-known/est path e.g. - You need to have an existing [CA hierarchy](/documentation/platform/pki/private-ca). - The client devices need to have a bootstrap/pre-installed certificate. -- The client devices must trust the server certificates used by Infisical's EST server. If the devices are new or lack existing trust configurations, you need to manually establish trust for the appropriate certificates. When using Infisical Cloud, this means establishing trust for certificates issued by AWS. +- The client devices must trust the server certificates used by Infisical's EST server. If the devices are new or lack existing trust configurations, you need to manually establish trust for the appropriate certificates. + - For Infisical Cloud users, the devices must be configured to trust the [Amazon root CA certificates](https://www.amazontrust.com/repository). ## Guide to configuring EST @@ -42,7 +43,7 @@ These endpoints are exposed on port 8443 under the .well-known/est path e.g. 4. Once the configuration of enrollment options is completed, a new **EST Label** field appears in the enrollment settings. This is the value to use as label in the URL when configuring the connection of EST clients to Infisical. ![est enrollment modal create](/images/platform/pki/est/template-enrollment-est-label.png) - For demonstration, the complete URL of the supported EST endpoints will look like the following: + The complete URL of the supported EST endpoints will look like the following: - https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/cacerts - https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/simpleenroll diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 91901c8bc..6ee571d93 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -41,4 +41,5 @@ export type SubscriptionPlan = { caCrl: boolean; instanceUserManagement: boolean; externalKms: boolean; + pkiEst: boolean; }; diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx index a3451bed2..4c3ca73fa 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; -import { Button, DeleteActionModal } from "@app/components/v2"; +import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useDeleteCertTemplate } from "@app/hooks/api"; @@ -16,7 +16,8 @@ export const CertificateTemplatesSection = () => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "certificateTemplate", "deleteCertificateTemplate", - "enrollmentOptions" + "enrollmentOptions", + "upgradePlan" ] as const); const { currentWorkspace } = useWorkspace(); @@ -85,6 +86,11 @@ export const CertificateTemplatesSection = () => { ) } /> + handlePopUpToggle("upgradePlan", isOpen)} + text="Managing template enrollment options for EST is only available on Infisical's Enterprise plan." + /> ); }; diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesTable.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesTable.tsx index e5abf4b0e..bfba64cd3 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesTable.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesTable.tsx @@ -19,14 +19,19 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useSubscription, + useWorkspace +} from "@app/context"; import { useListWorkspaceCertificateTemplates } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { handlePopUpOpen: ( popUpName: keyof UsePopUpState< - ["certificateTemplate", "deleteCertificateTemplate", "enrollmentOptions"] + ["certificateTemplate", "deleteCertificateTemplate", "enrollmentOptions", "upgradePlan"] >, data?: { id?: string; @@ -37,6 +42,7 @@ type Props = { export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => { const { currentWorkspace } = useWorkspace(); + const { subscription } = useSubscription(); const { data, isLoading } = useListWorkspaceCertificateTemplates({ workspaceId: currentWorkspace?.id ?? "" }); @@ -86,11 +92,16 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => { > {(isAllowed) => ( + onClick={() => { + if (!subscription?.pkiEst) { + handlePopUpOpen("upgradePlan"); + return; + } + handlePopUpOpen("enrollmentOptions", { id: certificateTemplate.id - }) - } + }); + }} className={twMerge( !isAllowed && "pointer-events-none cursor-not-allowed opacity-50" )}