diff --git a/backend/src/server/routes/v1/app-connection-routers/oci-connection-router.ts b/backend/src/ee/routes/v1/app-connection-routers/oci-connection-router.ts similarity index 94% rename from backend/src/server/routes/v1/app-connection-routers/oci-connection-router.ts rename to backend/src/ee/routes/v1/app-connection-routers/oci-connection-router.ts index d78eee3d9..e87e5b69e 100644 --- a/backend/src/server/routes/v1/app-connection-routers/oci-connection-router.ts +++ b/backend/src/ee/routes/v1/app-connection-routers/oci-connection-router.ts @@ -1,16 +1,16 @@ import z from "zod"; -import { readLimit } from "@app/server/config/rateLimiter"; -import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { CreateOCIConnectionSchema, SanitizedOCIConnectionSchema, UpdateOCIConnectionSchema -} from "@app/services/app-connection/oci"; +} from "@app/ee/services/app-connections/oci"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AuthMode } from "@app/services/auth/auth-type"; -import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; +import { registerAppConnectionEndpoints } from "../../../../server/routes/v1/app-connection-routers/app-connection-endpoints"; export const registerOCIConnectionRouter = async (server: FastifyZodProvider) => { registerAppConnectionEndpoints({ diff --git a/backend/src/services/app-connection/oci/index.ts b/backend/src/ee/services/app-connections/oci/index.ts similarity index 100% rename from backend/src/services/app-connection/oci/index.ts rename to backend/src/ee/services/app-connections/oci/index.ts diff --git a/backend/src/services/app-connection/oci/oci-connection-enums.ts b/backend/src/ee/services/app-connections/oci/oci-connection-enums.ts similarity index 100% rename from backend/src/services/app-connection/oci/oci-connection-enums.ts rename to backend/src/ee/services/app-connections/oci/oci-connection-enums.ts diff --git a/backend/src/services/app-connection/oci/oci-connection-fns.ts b/backend/src/ee/services/app-connections/oci/oci-connection-fns.ts similarity index 100% rename from backend/src/services/app-connection/oci/oci-connection-fns.ts rename to backend/src/ee/services/app-connections/oci/oci-connection-fns.ts diff --git a/backend/src/services/app-connection/oci/oci-connection-schemas.ts b/backend/src/ee/services/app-connections/oci/oci-connection-schemas.ts similarity index 100% rename from backend/src/services/app-connection/oci/oci-connection-schemas.ts rename to backend/src/ee/services/app-connections/oci/oci-connection-schemas.ts diff --git a/backend/src/services/app-connection/oci/oci-connection-service.ts b/backend/src/ee/services/app-connections/oci/oci-connection-service.ts similarity index 68% rename from backend/src/services/app-connection/oci/oci-connection-service.ts rename to backend/src/ee/services/app-connections/oci/oci-connection-service.ts index 2d72135e5..c2e60399c 100644 --- a/backend/src/services/app-connection/oci/oci-connection-service.ts +++ b/backend/src/ee/services/app-connections/oci/oci-connection-service.ts @@ -1,7 +1,9 @@ +import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { OrgServiceActor } from "@app/lib/types"; -import { AppConnection } from "../app-connection-enums"; +import { AppConnection } from "../../../../services/app-connection/app-connection-enums"; +import { TLicenseServiceFactory } from "../../license/license-service"; import { listOCICompartments, listOCIVaultKeys, listOCIVaults } from "./oci-connection-fns"; import { TOCIConnection } from "./oci-connection-types"; @@ -22,8 +24,23 @@ type TListOCIVaultKeysDTO = { vaultOcid: string; }; -export const ociConnectionService = (getAppConnection: TGetAppConnectionFunc) => { +// Enterprise check +export const checkPlan = async (licenseService: Pick, orgId: string) => { + const plan = await licenseService.getPlan(orgId); + if (!plan.enterpriseAppConnections) + throw new BadRequestError({ + message: + "Failed to use app connection due to plan restriction. Upgrade plan to access enterprise app connections." + }); +}; + +export const ociConnectionService = ( + getAppConnection: TGetAppConnectionFunc, + licenseService: Pick +) => { const listCompartments = async (connectionId: string, actor: OrgServiceActor) => { + await checkPlan(licenseService, actor.orgId); + const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor); try { @@ -36,6 +53,8 @@ export const ociConnectionService = (getAppConnection: TGetAppConnectionFunc) => }; const listVaults = async ({ connectionId, compartmentOcid }: TListOCIVaultsDTO, actor: OrgServiceActor) => { + await checkPlan(licenseService, actor.orgId); + const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor); try { @@ -51,6 +70,8 @@ export const ociConnectionService = (getAppConnection: TGetAppConnectionFunc) => { connectionId, compartmentOcid, vaultOcid }: TListOCIVaultKeysDTO, actor: OrgServiceActor ) => { + await checkPlan(licenseService, actor.orgId); + const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor); try { diff --git a/backend/src/services/app-connection/oci/oci-connection-types.ts b/backend/src/ee/services/app-connections/oci/oci-connection-types.ts similarity index 87% rename from backend/src/services/app-connection/oci/oci-connection-types.ts rename to backend/src/ee/services/app-connections/oci/oci-connection-types.ts index 74ddfe0c8..e07554f29 100644 --- a/backend/src/services/app-connection/oci/oci-connection-types.ts +++ b/backend/src/ee/services/app-connections/oci/oci-connection-types.ts @@ -2,7 +2,7 @@ import z from "zod"; import { DiscriminativePick } from "@app/lib/types"; -import { AppConnection } from "../app-connection-enums"; +import { AppConnection } from "../../../../services/app-connection/app-connection-enums"; import { CreateOCIConnectionSchema, OCIConnectionSchema, diff --git a/backend/src/ee/services/license/__mocks__/license-fns.ts b/backend/src/ee/services/license/__mocks__/license-fns.ts index 6a8f807ad..5259d4616 100644 --- a/backend/src/ee/services/license/__mocks__/license-fns.ts +++ b/backend/src/ee/services/license/__mocks__/license-fns.ts @@ -29,7 +29,9 @@ export const getDefaultOnPremFeatures = () => { secretApproval: true, secretRotation: true, caCrl: false, - sshHostGroups: false + sshHostGroups: false, + enterpriseSecretSyncs: false, + enterpriseAppConnections: false }; }; diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 9725b9b52..0b943be30 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -55,7 +55,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ kmip: false, gateway: false, sshHostGroups: false, - enterpriseSecretSyncs: false + enterpriseSecretSyncs: false, + enterpriseAppConnections: false }); export const setupLicenseRequestWithStore = (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 51db5cb18..9511771ff 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -73,6 +73,7 @@ export type TFeatureSet = { gateway: false; sshHostGroups: false; enterpriseSecretSyncs: false; + enterpriseAppConnections: false; }; export type TOrgPlansTableDTO = { diff --git a/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-fns.ts b/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-fns.ts index 152d02c51..5b05b2301 100644 --- a/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-fns.ts +++ b/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-fns.ts @@ -1,5 +1,6 @@ import { secrets, vault } from "oci-sdk"; +import { getOCIProvider } from "@app/ee/services/app-connections/oci"; import { TCreateOCIVaultVariable, TDeleteOCIVaultVariable, @@ -9,7 +10,6 @@ import { TUpdateOCIVaultVariable } from "@app/ee/services/secret-sync/oci-vault/oci-vault-sync-types"; import { delay } from "@app/lib/delay"; -import { getOCIProvider } from "@app/services/app-connection/oci"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; diff --git a/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-types.ts b/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-types.ts index c040cd0c0..8804b1322 100644 --- a/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-types.ts +++ b/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-types.ts @@ -1,7 +1,7 @@ import { SimpleAuthenticationDetailsProvider } from "oci-sdk"; import { z } from "zod"; -import { TOCIConnection } from "@app/services/app-connection/oci"; +import { TOCIConnection } from "@app/ee/services/app-connections/oci"; import { CreateOCIVaultSyncSchema, OCIVaultSyncListItemSchema, OCIVaultSyncSchema } from "./oci-vault-sync-schemas"; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 5196971f9..fd0f0142d 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1015,7 +1015,8 @@ export const registerRoutes = async ( secretVersionV2BridgeDAL, secretVersionTagV2BridgeDAL, resourceMetadataDAL, - appConnectionDAL + appConnectionDAL, + licenseService }); const secretQueueService = secretQueueFactory({ @@ -1632,7 +1633,8 @@ export const registerRoutes = async ( const appConnectionService = appConnectionServiceFactory({ appConnectionDAL, permissionService, - kmsService + kmsService, + licenseService }); const secretSyncService = secretSyncServiceFactory({ @@ -1643,7 +1645,8 @@ export const registerRoutes = async ( folderDAL, secretSyncQueue, projectBotService, - keyStore + keyStore, + licenseService }); const kmipService = kmipServiceFactory({ diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index b9ce3deb8..a05a2f263 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/ee/services/app-connections/oci"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags } from "@app/lib/api-docs"; import { readLimit } from "@app/server/config/rateLimiter"; @@ -38,7 +39,6 @@ import { } from "@app/services/app-connection/humanitec"; import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap"; import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql"; -import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/services/app-connection/oci"; import { PostgresConnectionListItemSchema, SanitizedPostgresConnectionSchema diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index 6f6fa1991..357c617e1 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -1,5 +1,6 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { registerOCIConnectionRouter } from "../../../../ee/routes/v1/app-connection-routers/oci-connection-router"; import { registerAuth0ConnectionRouter } from "./auth0-connection-router"; import { registerAwsConnectionRouter } from "./aws-connection-router"; import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router"; @@ -13,7 +14,6 @@ import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router"; import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; import { registerLdapConnectionRouter } from "./ldap-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; -import { registerOCIConnectionRouter } from "./oci-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router"; import { registerTeamCityConnectionRouter } from "./teamcity-connection-router"; import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 6e09f1293..afae27039 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -66,3 +66,8 @@ export enum AWSRegion { // South America SA_EAST_1 = "sa-east-1" // Sao Paulo } + +export enum AppConnectionPlanType { + Enterprise = "enterprise", + Regular = "regular" +} diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index f6fd894a6..e6d1b2c55 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -8,6 +8,11 @@ import { } from "@app/services/app-connection/shared/sql"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { + getOCIConnectionListItem, + OCIConnectionMethod, + validateOCIConnectionCredentials +} from "../../ee/services/app-connections/oci"; import { AppConnection } from "./app-connection-enums"; import { TAppConnectionServiceFactoryDep } from "./app-connection-service"; import { @@ -53,7 +58,6 @@ import { } from "./humanitec"; import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap"; import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; -import { getOCIConnectionListItem, OCIConnectionMethod, validateOCIConnectionCredentials } from "./oci"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; import { getTeamCityConnectionListItem, diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index c32336453..b4b968031 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -1,4 +1,4 @@ -import { AppConnection } from "./app-connection-enums"; +import { AppConnection, AppConnectionPlanType } from "./app-connection-enums"; export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.AWS]: "AWS", @@ -21,3 +21,25 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.TeamCity]: "TeamCity", [AppConnection.OCI]: "OCI" }; + +export const APP_CONNECTION_PLAN_MAP: Record = { + [AppConnection.AWS]: AppConnectionPlanType.Regular, + [AppConnection.GitHub]: AppConnectionPlanType.Regular, + [AppConnection.GCP]: AppConnectionPlanType.Regular, + [AppConnection.AzureKeyVault]: AppConnectionPlanType.Regular, + [AppConnection.AzureAppConfiguration]: AppConnectionPlanType.Regular, + [AppConnection.AzureClientSecrets]: AppConnectionPlanType.Regular, + [AppConnection.Databricks]: AppConnectionPlanType.Regular, + [AppConnection.Humanitec]: AppConnectionPlanType.Regular, + [AppConnection.TerraformCloud]: AppConnectionPlanType.Regular, + [AppConnection.Vercel]: AppConnectionPlanType.Regular, + [AppConnection.Postgres]: AppConnectionPlanType.Regular, + [AppConnection.MsSql]: AppConnectionPlanType.Regular, + [AppConnection.Camunda]: AppConnectionPlanType.Regular, + [AppConnection.Windmill]: AppConnectionPlanType.Regular, + [AppConnection.Auth0]: AppConnectionPlanType.Regular, + [AppConnection.HCVault]: AppConnectionPlanType.Regular, + [AppConnection.LDAP]: AppConnectionPlanType.Regular, + [AppConnection.TeamCity]: AppConnectionPlanType.Regular, + [AppConnection.OCI]: AppConnectionPlanType.Enterprise +}; diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 85b63138a..662edab07 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { generateHash } from "@app/lib/crypto/encryption"; @@ -17,9 +18,11 @@ import { import { auth0ConnectionService } from "@app/services/app-connection/auth0/auth0-connection-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { ValidateOCIConnectionCredentialsSchema } from "../../ee/services/app-connections/oci"; +import { ociConnectionService } from "../../ee/services/app-connections/oci/oci-connection-service"; import { TAppConnectionDALFactory } from "./app-connection-dal"; -import { AppConnection } from "./app-connection-enums"; -import { APP_CONNECTION_NAME_MAP } from "./app-connection-maps"; +import { AppConnection, AppConnectionPlanType } from "./app-connection-enums"; +import { APP_CONNECTION_NAME_MAP, APP_CONNECTION_PLAN_MAP } from "./app-connection-maps"; import { TAppConnection, TAppConnectionConfig, @@ -49,8 +52,6 @@ import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec"; import { humanitecConnectionService } from "./humanitec/humanitec-connection-service"; import { ValidateLdapConnectionCredentialsSchema } from "./ldap"; import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql"; -import { ValidateOCIConnectionCredentialsSchema } from "./oci"; -import { ociConnectionService } from "./oci/oci-connection-service"; import { ValidatePostgresConnectionCredentialsSchema } from "./postgres"; import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity"; import { teamcityConnectionService } from "./teamcity/teamcity-connection-service"; @@ -65,6 +66,7 @@ export type TAppConnectionServiceFactoryDep = { appConnectionDAL: TAppConnectionDALFactory; permissionService: Pick; kmsService: Pick; + licenseService: Pick; }; export type TAppConnectionServiceFactory = ReturnType; @@ -94,7 +96,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record { const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => { const { permission } = await permissionService.getOrgPermission( @@ -191,6 +194,16 @@ export const appConnectionServiceFactory = ({ OrgPermissionSubjects.AppConnections ); + // Enterprise check + if (APP_CONNECTION_PLAN_MAP[app] === AppConnectionPlanType.Enterprise) { + const plan = await licenseService.getPlan(actor.orgId); + if (!plan.enterpriseAppConnections) + throw new BadRequestError({ + message: + "Failed to create app connection due to plan restriction. Upgrade plan to access enterprise app connections." + }); + } + const validatedCredentials = await validateAppConnectionCredentials({ app, credentials, @@ -253,6 +266,16 @@ export const appConnectionServiceFactory = ({ if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); + // Enterprise check + if (APP_CONNECTION_PLAN_MAP[appConnection.app as AppConnection] === AppConnectionPlanType.Enterprise) { + const plan = await licenseService.getPlan(actor.orgId); + if (!plan.enterpriseAppConnections) + throw new BadRequestError({ + message: + "Failed to update app connection due to plan restriction. Upgrade plan to access enterprise app connections." + }); + } + const { permission } = await permissionService.getOrgPermission( actor.type, actor.id, @@ -399,6 +422,16 @@ export const appConnectionServiceFactory = ({ if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); + // Enterprise check + if (APP_CONNECTION_PLAN_MAP[app] === AppConnectionPlanType.Enterprise) { + const plan = await licenseService.getPlan(actor.orgId); + if (!plan.enterpriseAppConnections) + throw new BadRequestError({ + message: + "Failed to connect app connection due to plan restriction. Upgrade plan to access enterprise app connections." + }); + } + const { permission: orgPermission } = await permissionService.getOrgPermission( actor.type, actor.id, @@ -468,6 +501,6 @@ export const appConnectionServiceFactory = ({ hcvault: hcVaultConnectionService(connectAppConnectionById), windmill: windmillConnectionService(connectAppConnectionById), teamcity: teamcityConnectionService(connectAppConnectionById), - oci: ociConnectionService(connectAppConnectionById) + oci: ociConnectionService(connectAppConnectionById, licenseService) }; }; diff --git a/backend/src/services/app-connection/app-connection-types.ts b/backend/src/services/app-connection/app-connection-types.ts index 42ccfc84e..496f5032e 100644 --- a/backend/src/services/app-connection/app-connection-types.ts +++ b/backend/src/services/app-connection/app-connection-types.ts @@ -2,6 +2,12 @@ import { TAppConnectionDALFactory } from "@app/services/app-connection/app-conne import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + TOCIConnection, + TOCIConnectionConfig, + TOCIConnectionInput, + TValidateOCIConnectionCredentialsSchema +} from "../../ee/services/app-connections/oci"; import { AWSRegion } from "./app-connection-enums"; import { TAuth0Connection, @@ -76,12 +82,6 @@ import { TValidateLdapConnectionCredentialsSchema } from "./ldap"; import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql"; -import { - TOCIConnection, - TOCIConnectionConfig, - TOCIConnectionInput, - TValidateOCIConnectionCredentialsSchema -} from "./oci"; import { TPostgresConnection, TPostgresConnectionInput, diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index a0982c5b6..e829e2131 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -26,3 +26,8 @@ export enum SecretSyncImportBehavior { PrioritizeSource = "prioritize-source", PrioritizeDestination = "prioritize-destination" } + +export enum SecretSyncPlanType { + Enterprise = "enterprise", + Regular = "regular" +} diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 21cb912b4..5b2906f1c 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -1,5 +1,5 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums"; -import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { SecretSync, SecretSyncPlanType } from "@app/services/secret-sync/secret-sync-enums"; export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.AWSParameterStore]: "AWS Parameter Store", @@ -36,3 +36,21 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.TeamCity]: AppConnection.TeamCity, [SecretSync.OCIVault]: AppConnection.OCI }; + +export const SECRET_SYNC_PLAN_MAP: Record = { + [SecretSync.AWSParameterStore]: SecretSyncPlanType.Regular, + [SecretSync.AWSSecretsManager]: SecretSyncPlanType.Regular, + [SecretSync.GitHub]: SecretSyncPlanType.Regular, + [SecretSync.GCPSecretManager]: SecretSyncPlanType.Regular, + [SecretSync.AzureKeyVault]: SecretSyncPlanType.Regular, + [SecretSync.AzureAppConfiguration]: SecretSyncPlanType.Regular, + [SecretSync.Databricks]: SecretSyncPlanType.Regular, + [SecretSync.Humanitec]: SecretSyncPlanType.Regular, + [SecretSync.TerraformCloud]: SecretSyncPlanType.Regular, + [SecretSync.Camunda]: SecretSyncPlanType.Regular, + [SecretSync.Vercel]: SecretSyncPlanType.Regular, + [SecretSync.Windmill]: SecretSyncPlanType.Regular, + [SecretSync.HCVault]: SecretSyncPlanType.Regular, + [SecretSync.TeamCity]: SecretSyncPlanType.Regular, + [SecretSync.OCIVault]: SecretSyncPlanType.Enterprise +}; diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index 62b4ba3cc..e8b4d569e 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -5,8 +5,10 @@ import { Job } from "bullmq"; import { ProjectMembershipRole, SecretType } from "@app/db/schemas"; import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; +import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; @@ -29,11 +31,12 @@ import { TSecretSyncDALFactory } from "@app/services/secret-sync/secret-sync-dal import { SecretSync, SecretSyncImportBehavior, - SecretSyncInitialSyncBehavior + SecretSyncInitialSyncBehavior, + SecretSyncPlanType } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; import { parseSyncErrorMessage, SecretSyncFns } from "@app/services/secret-sync/secret-sync-fns"; -import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; +import { SECRET_SYNC_NAME_MAP, SECRET_SYNC_PLAN_MAP } from "@app/services/secret-sync/secret-sync-maps"; import { SecretSyncAction, SecretSyncStatus, @@ -93,6 +96,7 @@ type TSecretSyncQueueFactoryDep = { secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; resourceMetadataDAL: Pick; + licenseService: Pick; }; type SecretSyncActionJob = Job< @@ -133,7 +137,8 @@ export const secretSyncQueueFactory = ({ secretVersionTagDAL, secretVersionV2BridgeDAL, secretVersionTagV2BridgeDAL, - resourceMetadataDAL + resourceMetadataDAL, + licenseService }: TSecretSyncQueueFactoryDep) => { const appCfg = getConfig(); @@ -323,7 +328,22 @@ export const secretSyncQueueFactory = ({ secretSync: TSecretSyncWithCredentials, importBehavior: SecretSyncImportBehavior ): Promise => { - const { projectId, environment, folder } = secretSync; + const { + projectId, + environment, + folder, + destination, + connection: { orgId } + } = secretSync; + + // Enterprise Check + if (SECRET_SYNC_PLAN_MAP[destination] === SecretSyncPlanType.Enterprise) { + const plan = await licenseService.getPlan(orgId); + if (!plan.enterpriseSecretSyncs) + throw new BadRequestError({ + message: "Failed to import secrets due to plan restriction. Upgrade plan to access enterprise secret syncs." + }); + } if (!environment || !folder) throw new Error( @@ -400,6 +420,15 @@ export const secretSyncQueueFactory = ({ if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`); + // Enterprise Check + if (SECRET_SYNC_PLAN_MAP[secretSync.destination as SecretSync] === SecretSyncPlanType.Enterprise) { + const plan = await licenseService.getPlan(secretSync.connection.orgId); + if (!plan.enterpriseSecretSyncs) + throw new BadRequestError({ + message: "Failed to sync secrets due to plan restriction. Upgrade plan to access enterprise secret syncs." + }); + } + await secretSyncDAL.updateById(syncId, { syncStatus: SecretSyncStatus.Running }); @@ -659,6 +688,16 @@ export const secretSyncQueueFactory = ({ if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`); + // Enterprise Check + if (SECRET_SYNC_PLAN_MAP[secretSync.destination as SecretSync] === SecretSyncPlanType.Enterprise) { + const plan = await licenseService.getPlan(secretSync.connection.orgId); + if (!plan.enterpriseSecretSyncs) + throw new BadRequestError({ + message: + "Failed to access secret sync due to plan restriction. Upgrade plan to access enterprise secret syncs." + }); + } + await secretSyncDAL.updateById(syncId, { removeStatus: SecretSyncStatus.Running }); diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts index db350f785..2fc1f103d 100644 --- a/backend/src/services/secret-sync/secret-sync-service.ts +++ b/backend/src/services/secret-sync/secret-sync-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { @@ -15,7 +16,7 @@ import { OrgServiceActor } from "@app/lib/types"; import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; -import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { SecretSync, SecretSyncPlanType } from "@app/services/secret-sync/secret-sync-enums"; import { listSecretSyncOptions } from "@app/services/secret-sync/secret-sync-fns"; import { SecretSyncStatus, @@ -34,7 +35,7 @@ import { import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { TSecretSyncDALFactory } from "./secret-sync-dal"; -import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "./secret-sync-maps"; +import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP, SECRET_SYNC_PLAN_MAP } from "./secret-sync-maps"; import { TSecretSyncQueueFactory } from "./secret-sync-queue"; type TSecretSyncServiceFactoryDep = { @@ -49,6 +50,7 @@ type TSecretSyncServiceFactoryDep = { TSecretSyncQueueFactory, "queueSecretSyncSyncSecretsById" | "queueSecretSyncImportSecretsById" | "queueSecretSyncRemoveSecretsById" >; + licenseService: Pick; }; export type TSecretSyncServiceFactory = ReturnType; @@ -61,7 +63,8 @@ export const secretSyncServiceFactory = ({ appConnectionService, projectBotService, secretSyncQueue, - keyStore + keyStore, + licenseService }: TSecretSyncServiceFactoryDep) => { const listSecretSyncsByProjectId = async ( { projectId, destination }: TListSecretSyncsByProjectId, @@ -191,6 +194,16 @@ export const secretSyncServiceFactory = ({ { projectId, secretPath, environment, ...params }: TCreateSecretSyncDTO, actor: OrgServiceActor ) => { + // Enterprise check + if (SECRET_SYNC_PLAN_MAP[params.destination] === SecretSyncPlanType.Enterprise) { + const plan = await licenseService.getPlan(actor.orgId); + if (!plan.enterpriseSecretSyncs) + throw new BadRequestError({ + message: + "Failed to create secret sync due to plan restriction. Upgrade plan to access enterprise secret syncs." + }); + } + const { permission: projectPermission } = await permissionService.getProjectPermission({ actor: actor.type, actorId: actor.id, @@ -260,6 +273,16 @@ export const secretSyncServiceFactory = ({ message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID ${syncId}` }); + // Enterprise check + if (SECRET_SYNC_PLAN_MAP[secretSync.destination as SecretSync] === SecretSyncPlanType.Enterprise) { + const plan = await licenseService.getPlan(actor.orgId); + if (!plan.enterpriseSecretSyncs) + throw new BadRequestError({ + message: + "Failed to update secret sync due to plan restriction. Upgrade plan to access enterprise secret syncs." + }); + } + const { permission } = await permissionService.getProjectPermission({ actor: actor.type, actorId: actor.id, @@ -408,6 +431,16 @@ export const secretSyncServiceFactory = ({ message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"` }); + // Enterprise check + if (SECRET_SYNC_PLAN_MAP[secretSync.destination as SecretSync] === SecretSyncPlanType.Enterprise) { + const plan = await licenseService.getPlan(actor.orgId); + if (!plan.enterpriseSecretSyncs) + throw new BadRequestError({ + message: + "Failed to trigger secret sync due to plan restriction. Upgrade plan to access enterprise secret syncs." + }); + } + const { permission } = await permissionService.getProjectPermission({ actor: actor.type, actorId: actor.id, @@ -463,6 +496,16 @@ export const secretSyncServiceFactory = ({ message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"` }); + // Enterprise check + if (SECRET_SYNC_PLAN_MAP[secretSync.destination as SecretSync] === SecretSyncPlanType.Enterprise) { + const plan = await licenseService.getPlan(actor.orgId); + if (!plan.enterpriseSecretSyncs) + throw new BadRequestError({ + message: + "Failed to trigger secret sync due to plan restriction. Upgrade plan to access enterprise secret syncs." + }); + } + const { permission } = await permissionService.getProjectPermission({ actor: actor.type, actorId: actor.id, @@ -512,6 +555,16 @@ export const secretSyncServiceFactory = ({ message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"` }); + // Enterprise check + if (SECRET_SYNC_PLAN_MAP[secretSync.destination as SecretSync] === SecretSyncPlanType.Enterprise) { + const plan = await licenseService.getPlan(actor.orgId); + if (!plan.enterpriseSecretSyncs) + throw new BadRequestError({ + message: + "Failed to trigger secret sync due to plan restriction. Upgrade plan to access enterprise secret syncs." + }); + } + const { permission } = await permissionService.getProjectPermission({ actor: actor.type, actorId: actor.id, diff --git a/docs/integrations/app-connections/oci.mdx b/docs/integrations/app-connections/oci.mdx index ff51ce1d9..58fb3c1d3 100644 --- a/docs/integrations/app-connections/oci.mdx +++ b/docs/integrations/app-connections/oci.mdx @@ -3,6 +3,13 @@ title: "OCI Connection" description: "Learn how to configure an Oracle Cloud Infrastructure Connection for Infisical." --- + + OCI App Connection is a paid feature. + + If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, + then you should contact team@infisical.com to purchase an enterprise license to use it. + + Infisical supports the use of [API Signing Key Authentication](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/apisigningkey.htm) to connect with OCI. ## Create OCI User diff --git a/docs/integrations/secret-syncs/oci-vault.mdx b/docs/integrations/secret-syncs/oci-vault.mdx index 67a3426aa..00b7120e7 100644 --- a/docs/integrations/secret-syncs/oci-vault.mdx +++ b/docs/integrations/secret-syncs/oci-vault.mdx @@ -3,6 +3,13 @@ title: "OCI Vault Sync" description: "Learn how to configure an Oracle Cloud Infrastructure Vault Sync for Infisical." --- + + OCI Vault Sync is a paid feature. + + If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, + then you should contact team@infisical.com to purchase an enterprise license to use it. + + **Prerequisites:** - Create an [OCI Connection](/integrations/app-connections/oci) with the required **Secret Sync** permissions - [Create](https://docs.oracle.com/en-us/iaas/Content/Identity/compartments/To_create_a_compartment.htm) or use an existing OCI Compartment (which the OCI Connection is authorized to access) diff --git a/frontend/src/components/secret-syncs/SecretSyncSelect.tsx b/frontend/src/components/secret-syncs/SecretSyncSelect.tsx index 428c1bf2d..1b2fe9c5b 100644 --- a/frontend/src/components/secret-syncs/SecretSyncSelect.tsx +++ b/frontend/src/components/secret-syncs/SecretSyncSelect.tsx @@ -1,13 +1,14 @@ import { faWrench } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; import { Spinner, Tooltip } from "@app/components/v2"; -import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; -import { SecretSync, useSecretSyncOptions } from "@app/hooks/api/secretSyncs"; -import { twMerge } from "tailwind-merge"; -import { UpgradePlanModal } from "../license/UpgradePlanModal"; -import { usePopUp } from "@app/hooks"; import { useSubscription } from "@app/context"; +import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; +import { usePopUp } from "@app/hooks"; +import { SecretSync, useSecretSyncOptions } from "@app/hooks/api/secretSyncs"; + +import { UpgradePlanModal } from "../license/UpgradePlanModal"; type Props = { onSelect: (destination: SecretSync) => void; @@ -52,7 +53,7 @@ export const SecretSyncSelect = ({ onSelect }: Props) => { )} > {enterprise && !subscription.enterpriseSecretSyncs && ( -
+
)} = { [AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" }, [AppConnection.GitHub]: { name: "GitHub", image: "GitHub.png" }, @@ -63,7 +63,7 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.HCVault]: { name: "Hashicorp Vault", image: "Vault.png", size: 65 }, [AppConnection.LDAP]: { name: "LDAP", image: "LDAP.png", size: 65 }, [AppConnection.TeamCity]: { name: "TeamCity", image: "TeamCity.png" }, - [AppConnection.OCI]: { name: "OCI", image: "Oracle.png" } + [AppConnection.OCI]: { name: "OCI", image: "Oracle.png", enterprise: true } }; export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index a8ca35a92..a861c215a 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -51,4 +51,5 @@ export type SubscriptionPlan = { projectTemplates: boolean; kmip: boolean; enterpriseSecretSyncs: boolean; + enterpriseAppConnections: boolean; }; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx index 5e9f69335..1019d2225 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx @@ -1,8 +1,12 @@ import { faWrench } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; +import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { Spinner, Tooltip } from "@app/components/v2"; +import { useSubscription } from "@app/context"; import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; +import { usePopUp } from "@app/hooks"; import { useAppConnectionOptions } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; @@ -11,8 +15,11 @@ type Props = { }; export const AppConnectionsSelect = ({ onSelect }: Props) => { + const { subscription } = useSubscription(); const { isPending, data: appConnectionOptions } = useAppConnectionOptions(); + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const); + if (isPending) { return (
@@ -25,29 +32,52 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => { return (
{appConnectionOptions?.map((option) => { - const { image, name, size = 50 } = APP_CONNECTION_MAP[option.app]; + const { image, name, size = 50, enterprise = false } = APP_CONNECTION_MAP[option.app]; return ( - + + ); })} + handlePopUpToggle("upgradePlan", isOpen)} + text="You can use every App Connection if you switch to Infisical's Enterprise plan." + />