From aab204a68a4d3d3def6f04d0c5f97a016886c57c Mon Sep 17 00:00:00 2001 From: x032205 Date: Wed, 16 Jul 2025 01:33:22 -0400 Subject: [PATCH 1/9] feat(app-connection): Gateway support for SQL connections --- .../oracledb/oracledb-connection-schemas.ts | 1 + backend/src/server/routes/index.ts | 3 +- .../app-connection/app-connection-service.ts | 11 ++- .../app-connection/app-connection-types.ts | 4 +- .../mssql/mssql-connection-schemas.ts | 1 + .../mysql/mysql-connection-schemas.ts | 1 + .../postgres/postgres-connection-schemas.ts | 1 + .../shared/sql/sql-connection-fns.ts | 98 +++++++++++++++---- .../shared/sql/sql-connection-schemas.ts | 1 + .../types/shared/sql-connection.ts | 1 + .../AppConnectionForm/MsSqlConnectionForm.tsx | 60 +++++++++++- .../AppConnectionForm/MySqlConnectionForm.tsx | 60 +++++++++++- .../OracleDBConnectionForm.tsx | 60 +++++++++++- .../PostgresConnectionForm.tsx | 60 +++++++++++- .../shared/sql-connection-schemas.ts | 1 + 15 files changed, 333 insertions(+), 30 deletions(-) diff --git a/backend/src/ee/services/app-connections/oracledb/oracledb-connection-schemas.ts b/backend/src/ee/services/app-connections/oracledb/oracledb-connection-schemas.ts index f93abae83..ed95ba0da 100644 --- a/backend/src/ee/services/app-connections/oracledb/oracledb-connection-schemas.ts +++ b/backend/src/ee/services/app-connections/oracledb/oracledb-connection-schemas.ts @@ -24,6 +24,7 @@ export const SanitizedOracleDBConnectionSchema = z.discriminatedUnion("method", BaseOracleDBConnectionSchema.extend({ method: z.literal(OracleDBConnectionMethod.UsernameAndPassword), credentials: OracleDBConnectionCredentialsSchema.pick({ + gatewayId: true, host: true, database: true, port: true, diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index dcaa2b654..9fc796554 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1706,7 +1706,8 @@ export const registerRoutes = async ( appConnectionDAL, permissionService, kmsService, - licenseService + licenseService, + gatewayService }); const secretSyncService = secretSyncServiceFactory({ diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index fdb861b95..faa813557 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -3,6 +3,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci"; import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service"; import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb"; +import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; 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-types"; @@ -92,6 +93,7 @@ export type TAppConnectionServiceFactoryDep = { permissionService: Pick; kmsService: Pick; licenseService: Pick; + gatewayService: Pick; }; export type TAppConnectionServiceFactory = ReturnType; @@ -135,7 +137,8 @@ export const appConnectionServiceFactory = ({ appConnectionDAL, permissionService, kmsService, - licenseService + licenseService, + gatewayService }: TAppConnectionServiceFactoryDep) => { const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => { const { permission } = await permissionService.getOrgPermission( @@ -273,7 +276,8 @@ export const appConnectionServiceFactory = ({ credentials: validatedCredentials, method } as TAppConnectionConfig, - (platformCredentials) => createConnection(platformCredentials) + (platformCredentials) => createConnection(platformCredentials), + gatewayService ); } else { connection = await createConnection(validatedCredentials); @@ -387,7 +391,8 @@ export const appConnectionServiceFactory = ({ credentials: updatedCredentials, method } as TAppConnectionConfig, - (platformCredentials) => updateConnection(platformCredentials) + (platformCredentials) => updateConnection(platformCredentials), + gatewayService ); } else { updatedConnection = await updateConnection(updatedCredentials); diff --git a/backend/src/services/app-connection/app-connection-types.ts b/backend/src/services/app-connection/app-connection-types.ts index 4c9be6b8e..ffc814529 100644 --- a/backend/src/services/app-connection/app-connection-types.ts +++ b/backend/src/services/app-connection/app-connection-types.ts @@ -9,6 +9,7 @@ import { TOracleDBConnectionInput, TValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb"; +import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; @@ -354,7 +355,8 @@ export type TAppConnectionCredentialsValidator = ( export type TAppConnectionTransitionCredentialsToPlatform = ( appConnection: TAppConnectionConfig, - callback: (credentials: TAppConnection["credentials"]) => Promise + callback: (credentials: TAppConnection["credentials"]) => Promise, + gatewayService: Pick ) => Promise; export type TAppConnectionBaseConfig = { diff --git a/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts b/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts index 994f9a40d..e48b42286 100644 --- a/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts +++ b/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts @@ -26,6 +26,7 @@ export const SanitizedMsSqlConnectionSchema = z.discriminatedUnion("method", [ BaseMsSqlConnectionSchema.extend({ method: z.literal(MsSqlConnectionMethod.UsernameAndPassword), credentials: MsSqlConnectionAccessTokenCredentialsSchema.pick({ + gatewayId: true, host: true, database: true, port: true, diff --git a/backend/src/services/app-connection/mysql/mysql-connection-schemas.ts b/backend/src/services/app-connection/mysql/mysql-connection-schemas.ts index 082bac557..0196f2f02 100644 --- a/backend/src/services/app-connection/mysql/mysql-connection-schemas.ts +++ b/backend/src/services/app-connection/mysql/mysql-connection-schemas.ts @@ -24,6 +24,7 @@ export const SanitizedMySqlConnectionSchema = z.discriminatedUnion("method", [ BaseMySqlConnectionSchema.extend({ method: z.literal(MySqlConnectionMethod.UsernameAndPassword), credentials: MySqlConnectionAccessTokenCredentialsSchema.pick({ + gatewayId: true, host: true, database: true, port: true, diff --git a/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts b/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts index 1ddf1e2da..a9edf7710 100644 --- a/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts +++ b/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts @@ -24,6 +24,7 @@ export const SanitizedPostgresConnectionSchema = z.discriminatedUnion("method", BasePostgresConnectionSchema.extend({ method: z.literal(PostgresConnectionMethod.UsernameAndPassword), credentials: PostgresConnectionAccessTokenCredentialsSchema.pick({ + gatewayId: true, host: true, database: true, port: true, diff --git a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts index 33cc8257d..86953edce 100644 --- a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts +++ b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts @@ -1,11 +1,13 @@ import knex, { Knex } from "knex"; import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns"; +import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TSqlCredentialsRotationGeneratedCredentials, TSqlCredentialsRotationWithConnection } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types"; import { BadRequestError, DatabaseError } from "@app/lib/errors"; +import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { TAppConnectionRaw, TSqlConnection } from "@app/services/app-connection/app-connection-types"; @@ -98,25 +100,80 @@ export const getSqlConnectionClient = async (appConnection: Pick { +const executeWithPotentialGateway = async ( + config: TSqlConnectionConfig, + gatewayService: Pick, + operation: (client: Knex) => Promise +): Promise => { const { credentials, app } = config; - let client: Knex | undefined; + if (credentials.gatewayId && gatewayService) { + const [targetHost] = await verifyHostInputValidity(credentials.host, true); + const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(credentials.gatewayId); + const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); + return withGatewayProxy( + async (proxyPort) => { + const client = knex({ + client: SQL_CONNECTION_CLIENT_MAP[app], + connection: { + database: credentials.database, + port: proxyPort, + host: "localhost", + user: credentials.username, + password: credentials.password, + connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT, + ...getConnectionConfig({ app, credentials }) + } + }); + try { + return await operation(client); + } finally { + await client.destroy(); + } + }, + { + protocol: GatewayProxyProtocol.Tcp, + targetHost, + targetPort: credentials.port, + relayHost, + relayPort: Number(relayPort), + identityId: relayDetails.identityId, + orgId: relayDetails.orgId, + tlsOptions: { + ca: relayDetails.certChain, + cert: relayDetails.certificate, + key: relayDetails.privateKey.toString() + } + } + ); + } + + // Non-gateway path + const client = await getSqlConnectionClient({ app, credentials }); try { - client = await getSqlConnectionClient({ app, credentials }); + return await operation(client); + } finally { + await client.destroy(); + } +}; - await client.raw(`Select 1`); - - return credentials; +export const validateSqlConnectionCredentials = async ( + config: TSqlConnectionConfig & { gatewayId?: string }, + gatewayService: Pick +) => { + try { + await executeWithPotentialGateway(config, gatewayService, async (client) => { + await client.raw(`Select 1`); + }); + return config.credentials; } catch (error) { throw new BadRequestError({ message: `Unable to validate connection: ${ - (error as Error)?.message?.replaceAll(credentials.password, "********************") ?? "verify credentials" + (error as Error)?.message?.replaceAll(config.credentials.password, "********************") ?? + "verify credentials" }` }); - } finally { - await client?.destroy(); } }; @@ -132,22 +189,23 @@ export const SQL_CONNECTION_ALTER_LOGIN_STATEMENT: Record< export const transferSqlConnectionCredentialsToPlatform = async ( config: TSqlConnectionConfig, - callback: (credentials: TSqlConnectionConfig["credentials"]) => Promise + callback: (credentials: TSqlConnectionConfig["credentials"]) => Promise, + gatewayService: Pick ) => { const { credentials, app } = config; - const client = await getSqlConnectionClient({ app, credentials }); - const newPassword = alphaNumericNanoId(32); try { - return await client.transaction(async (tx) => { - await tx.raw( - ...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[app]({ username: credentials.username, password: newPassword }) - ); - return callback({ - ...credentials, - password: newPassword + return await executeWithPotentialGateway(config, gatewayService, (client) => { + return client.transaction(async (tx) => { + await tx.raw( + ...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[app]({ username: credentials.username, password: newPassword }) + ); + return callback({ + ...credentials, + password: newPassword + }); }); }); } catch (error) { @@ -161,7 +219,5 @@ export const transferSqlConnectionCredentialsToPlatform = async ( (error as Error)?.message?.replaceAll(newPassword, "********************") ?? "Encountered an error transferring credentials to platform" }); - } finally { - await client.destroy(); } }; diff --git a/backend/src/services/app-connection/shared/sql/sql-connection-schemas.ts b/backend/src/services/app-connection/shared/sql/sql-connection-schemas.ts index 500ed596a..bfaf6f2bc 100644 --- a/backend/src/services/app-connection/shared/sql/sql-connection-schemas.ts +++ b/backend/src/services/app-connection/shared/sql/sql-connection-schemas.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { AppConnections } from "@app/lib/api-docs"; export const BaseSqlUsernameAndPasswordConnectionSchema = z.object({ + gatewayId: z.string().optional(), host: z.string().trim().min(1, "Host required").describe(AppConnections.CREDENTIALS.SQL_CONNECTION.host), port: z.coerce.number().describe(AppConnections.CREDENTIALS.SQL_CONNECTION.port), database: z.string().trim().min(1, "Database required").describe(AppConnections.CREDENTIALS.SQL_CONNECTION.database), diff --git a/frontend/src/hooks/api/appConnections/types/shared/sql-connection.ts b/frontend/src/hooks/api/appConnections/types/shared/sql-connection.ts index d0e71158e..f88d8f726 100644 --- a/frontend/src/hooks/api/appConnections/types/shared/sql-connection.ts +++ b/frontend/src/hooks/api/appConnections/types/shared/sql-connection.ts @@ -1,4 +1,5 @@ export type TBaseSqlConnectionCredentials = { + gatewayId?: string; host: string; port: number; username: string; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MsSqlConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MsSqlConnectionForm.tsx index f7d48744d..721ff30d4 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MsSqlConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MsSqlConnectionForm.tsx @@ -1,10 +1,17 @@ import { useState } from "react"; import { Controller, FormProvider, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; import { z } from "zod"; -import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button, FormControl, ModalClose, Select, SelectItem, Tooltip } from "@app/components/v2"; +import { + OrgGatewayPermissionActions, + OrgPermissionSubjects +} from "@app/context/OrgPermissionContext/types"; import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { gatewaysQueryKeys } from "@app/hooks/api"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { MsSqlConnectionMethod, @@ -52,6 +59,7 @@ export const MsSqlConnectionForm = ({ appConnection, onSubmit }: Props) => { app: AppConnection.MsSql, method: MsSqlConnectionMethod.UsernameAndPassword, credentials: { + gatewayId: "", host: "", port: 1433, database: "default", @@ -71,6 +79,7 @@ export const MsSqlConnectionForm = ({ appConnection, onSubmit }: Props) => { } = form; const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false; + const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); const confirmSubmit = async (formData: FormData) => { if (formData.isPlatformManagedCredentials) { @@ -121,6 +130,55 @@ export const MsSqlConnectionForm = ({ appConnection, onSubmit }: Props) => { )} /> + + {(isAllowed) => ( + ( + + +
+ +
+
+
+ )} + /> + )} +
{ app: AppConnection.MySql, method: MySqlConnectionMethod.UsernameAndPassword, credentials: { + gatewayId: "", host: "", port: 3306, database: "default", @@ -68,6 +76,7 @@ export const MySqlConnectionForm = ({ appConnection, onSubmit }: Props) => { } = form; const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false; + const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); const confirmSubmit = async (formData: FormData) => { if (formData.isPlatformManagedCredentials) { @@ -118,6 +127,55 @@ export const MySqlConnectionForm = ({ appConnection, onSubmit }: Props) => { )} /> + + {(isAllowed) => ( + ( + + +
+ +
+
+
+ )} + /> + )} +
{ app: AppConnection.OracleDB, method: OracleDBConnectionMethod.UsernameAndPassword, credentials: { + gatewayId: "", host: "", port: 1521, database: "ORCL", // Typically FREEPDB1 or ORCL @@ -68,6 +76,7 @@ export const OracleDBConnectionForm = ({ appConnection, onSubmit }: Props) => { } = form; const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false; + const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); const confirmSubmit = async (formData: FormData) => { if (formData.isPlatformManagedCredentials) { @@ -118,6 +127,55 @@ export const OracleDBConnectionForm = ({ appConnection, onSubmit }: Props) => { )} /> + + {(isAllowed) => ( + ( + + +
+ +
+
+
+ )} + /> + )} +
{ app: AppConnection.Postgres, method: PostgresConnectionMethod.UsernameAndPassword, credentials: { + gatewayId: "", host: "", port: 5432, database: "default", @@ -68,6 +76,7 @@ export const PostgresConnectionForm = ({ appConnection, onSubmit }: Props) => { } = form; const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false; + const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); const confirmSubmit = async (formData: FormData) => { if (formData.isPlatformManagedCredentials) { @@ -118,6 +127,55 @@ export const PostgresConnectionForm = ({ appConnection, onSubmit }: Props) => { )} /> + + {(isAllowed) => ( + ( + + +
+ +
+
+
+ )} + /> + )} +
Date: Wed, 16 Jul 2025 02:24:16 -0400 Subject: [PATCH 2/9] Make sql secret rotation use gateway --- .../secret-rotation-v2-service.ts | 13 ++- .../secret-rotation-v2-types.ts | 4 +- .../sql-credentials-rotation-fns.ts | 80 +++++++++++-------- backend/src/server/routes/index.ts | 3 +- .../app-connection/app-connection-fns.ts | 6 +- .../app-connection/app-connection-service.ts | 30 ++++--- .../app-connection/app-connection-types.ts | 3 +- .../shared/sql/sql-connection-fns.ts | 4 +- 8 files changed, 85 insertions(+), 58 deletions(-) diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts index cd88f889e..b1cc8ad23 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts @@ -107,6 +107,7 @@ export type TSecretRotationV2ServiceFactoryDep = { queueService: Pick; appConnectionDAL: Pick; folderCommitService: Pick; + gatewayService: Pick; }; export type TSecretRotationV2ServiceFactory = ReturnType; @@ -148,7 +149,8 @@ export const secretRotationV2ServiceFactory = ({ keyStore, queueService, folderCommitService, - appConnectionDAL + appConnectionDAL, + gatewayService }: TSecretRotationV2ServiceFactoryDep) => { const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => { const appCfg = getConfig(); @@ -461,7 +463,8 @@ export const secretRotationV2ServiceFactory = ({ rotationInterval: payload.rotationInterval } as TSecretRotationV2WithConnection, appConnectionDAL, - kmsService + kmsService, + gatewayService ); // even though we have a db constraint we want to check before any rotation of credentials is attempted @@ -824,7 +827,8 @@ export const secretRotationV2ServiceFactory = ({ connection: appConnection } as TSecretRotationV2WithConnection, appConnectionDAL, - kmsService + kmsService, + gatewayService ); const generatedCredentials = await decryptSecretRotationCredentials({ @@ -907,7 +911,8 @@ export const secretRotationV2ServiceFactory = ({ connection: appConnection } as TSecretRotationV2WithConnection, appConnectionDAL, - kmsService + kmsService, + gatewayService ); const updatedRotation = await rotationFactory.rotateCredentials( diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts index 5547d4582..b90d0ed88 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts @@ -1,4 +1,5 @@ import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types"; +import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TSqlCredentialsRotationGeneratedCredentials } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types"; import { OrderByDirection } from "@app/lib/types"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; @@ -239,7 +240,8 @@ export type TRotationFactory< > = ( secretRotation: T, appConnectionDAL: Pick, - kmsService: Pick + kmsService: Pick, + gatewayService: Pick ) => { issueCredentials: TRotationFactoryIssueCredentials; revokeCredentials: TRotationFactoryRevokeCredentials; diff --git a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts index 12e9b5964..3e6e5d265 100644 --- a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts @@ -1,3 +1,5 @@ +import { Knex } from "knex"; + import { TRotationFactory, TRotationFactoryGetSecretsPayload, @@ -5,7 +7,10 @@ import { TRotationFactoryRevokeCredentials, TRotationFactoryRotateCredentials } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; -import { getSqlConnectionClient, SQL_CONNECTION_ALTER_LOGIN_STATEMENT } from "@app/services/app-connection/shared/sql"; +import { + executeWithPotentialGateway, + SQL_CONNECTION_ALTER_LOGIN_STATEMENT +} from "@app/services/app-connection/shared/sql"; import { generatePassword } from "../utils"; import { @@ -30,7 +35,7 @@ const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGenerat export const sqlCredentialsRotationFactory: TRotationFactory< TSqlCredentialsRotationWithConnection, TSqlCredentialsRotationGeneratedCredentials -> = (secretRotation) => { +> = (secretRotation, _appConnectionDAL, _kmsService, gatewayService) => { const { connection, parameters: { username1, username2 }, @@ -38,29 +43,38 @@ export const sqlCredentialsRotationFactory: TRotationFactory< secretsMapping } = secretRotation; - const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => { - const client = await getSqlConnectionClient({ - ...connection, - credentials: { - ...connection.credentials, - ...credentials - } - }); + const executeOperation = ( + operation: (client: Knex) => Promise, + credentialsOverride?: TSqlCredentialsRotationGeneratedCredentials[number] + ) => { + const finalCredentials = { + ...connection.credentials, + ...credentialsOverride + }; + return executeWithPotentialGateway( + { + ...connection, + credentials: finalCredentials + }, + gatewayService, + (client) => operation(client) + ); + }; + + const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => { try { - await client.raw("SELECT 1"); + await executeOperation(async (client) => { + await client.raw("SELECT 1"); + }, credentials); } catch (error) { throw new Error(redactPasswords(error, [credentials])); - } finally { - await client.destroy(); } }; const issueCredentials: TRotationFactoryIssueCredentials = async ( callback ) => { - const client = await getSqlConnectionClient(connection); - // For SQL, since we get existing users, we change both their passwords // on issue to invalidate their existing passwords const credentialsSet = [ @@ -69,15 +83,15 @@ export const sqlCredentialsRotationFactory: TRotationFactory< ]; try { - await client.transaction(async (tx) => { - for await (const credentials of credentialsSet) { - await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials)); - } + await executeOperation(async (client) => { + await client.transaction(async (tx) => { + for await (const credentials of credentialsSet) { + await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials)); + } + }); }); } catch (error) { throw new Error(redactPasswords(error, credentialsSet)); - } finally { - await client.destroy(); } for await (const credentials of credentialsSet) { @@ -91,21 +105,19 @@ export const sqlCredentialsRotationFactory: TRotationFactory< credentialsToRevoke, callback ) => { - const client = await getSqlConnectionClient(connection); - const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() })); try { - await client.transaction(async (tx) => { - for await (const credentials of revokedCredentials) { - // invalidate previous passwords - await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials)); - } + await executeOperation(async (client) => { + await client.transaction(async (tx) => { + for await (const credentials of revokedCredentials) { + // invalidate previous passwords + await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials)); + } + }); }); } catch (error) { throw new Error(redactPasswords(error, revokedCredentials)); - } finally { - await client.destroy(); } return callback(); @@ -115,17 +127,15 @@ export const sqlCredentialsRotationFactory: TRotationFactory< _, callback ) => { - const client = await getSqlConnectionClient(connection); - // generate new password for the next active user const credentials = { username: activeIndex === 0 ? username2 : username1, password: generatePassword() }; try { - await client.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials)); + await executeOperation(async (client) => { + await client.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials)); + }); } catch (error) { throw new Error(redactPasswords(error, [credentials])); - } finally { - await client.destroy(); } await $validateCredentials(credentials); diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 9fc796554..fbfcd2177 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1805,7 +1805,8 @@ export const registerRoutes = async ( snapshotService, secretQueueService, queueService, - appConnectionDAL + appConnectionDAL, + gatewayService }); const certificateAuthorityService = certificateAuthorityServiceFactory({ diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 9dadcc4e5..c2290af11 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -5,6 +5,7 @@ import { validateOCIConnectionCredentials } from "@app/ee/services/app-connections/oci"; import { getOracleDBConnectionListItem, OracleDBConnectionMethod } from "@app/ee/services/app-connections/oracledb"; +import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError } from "@app/lib/errors"; @@ -193,7 +194,8 @@ export const decryptAppConnectionCredentials = async ({ }; export const validateAppConnectionCredentials = async ( - appConnection: TAppConnectionConfig + appConnection: TAppConnectionConfig, + gatewayService: Pick ): Promise => { const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record = { [AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator, @@ -232,7 +234,7 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator }; - return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); + return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService); }; export const getAppConnectionMethodName = (method: TAppConnection["method"]) => { diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index faa813557..b4ffb27a0 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -242,12 +242,15 @@ export const appConnectionServiceFactory = ({ "Failed to create app connection due to plan restriction. Upgrade plan to access enterprise app connections." ); - const validatedCredentials = await validateAppConnectionCredentials({ - app, - credentials, - method, - orgId: actor.orgId - } as TAppConnectionConfig); + const validatedCredentials = await validateAppConnectionCredentials( + { + app, + credentials, + method, + orgId: actor.orgId + } as TAppConnectionConfig, + gatewayService + ); try { const createConnection = async (connectionCredentials: TAppConnection["credentials"]) => { @@ -349,12 +352,15 @@ export const appConnectionServiceFactory = ({ } Connection with method ${getAppConnectionMethodName(method)}` }); - updatedCredentials = await validateAppConnectionCredentials({ - app, - orgId: actor.orgId, - credentials, - method - } as TAppConnectionConfig); + updatedCredentials = await validateAppConnectionCredentials( + { + app, + orgId: actor.orgId, + credentials, + method + } as TAppConnectionConfig, + gatewayService + ); if (!updatedCredentials) throw new BadRequestError({ message: "Unable to validate connection - check credentials" }); diff --git a/backend/src/services/app-connection/app-connection-types.ts b/backend/src/services/app-connection/app-connection-types.ts index ffc814529..211944f19 100644 --- a/backend/src/services/app-connection/app-connection-types.ts +++ b/backend/src/services/app-connection/app-connection-types.ts @@ -350,7 +350,8 @@ export type TListAwsConnectionIamUsers = { }; export type TAppConnectionCredentialsValidator = ( - appConnection: TAppConnectionConfig + appConnection: TAppConnectionConfig, + gatewayService: Pick ) => Promise; export type TAppConnectionTransitionCredentialsToPlatform = ( diff --git a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts index 86953edce..49204e9e4 100644 --- a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts +++ b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts @@ -100,7 +100,7 @@ export const getSqlConnectionClient = async (appConnection: Pick( +export const executeWithPotentialGateway = async ( config: TSqlConnectionConfig, gatewayService: Pick, operation: (client: Knex) => Promise @@ -159,7 +159,7 @@ const executeWithPotentialGateway = async ( }; export const validateSqlConnectionCredentials = async ( - config: TSqlConnectionConfig & { gatewayId?: string }, + config: TSqlConnectionConfig, gatewayService: Pick ) => { try { From 19cb2201077a94735494e7ab0789d1d3262453be Mon Sep 17 00:00:00 2001 From: x032205 Date: Wed, 16 Jul 2025 03:05:32 -0400 Subject: [PATCH 3/9] A few tweaks --- backend/src/lib/api-docs/constants.ts | 1 + .../app-connection/shared/sql/sql-connection-schemas.ts | 6 +++++- .../components/AppConnectionForm/MsSqlConnectionForm.tsx | 2 +- .../components/AppConnectionForm/MySqlConnectionForm.tsx | 2 +- .../components/AppConnectionForm/OracleDBConnectionForm.tsx | 2 +- .../components/AppConnectionForm/PostgresConnectionForm.tsx | 2 +- .../AppConnectionForm/shared/sql-connection-schemas.ts | 2 +- 7 files changed, 11 insertions(+), 6 deletions(-) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 59734583a..657729b32 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2199,6 +2199,7 @@ export const AppConnections = { audience: "The unique identifier of the target API you want to access." }, SQL_CONNECTION: { + gatewayId: "The ID of the gateway to use when performing database requests.", host: "The hostname of the database server.", port: "The port number of the database.", database: "The name of the database to connect to.", diff --git a/backend/src/services/app-connection/shared/sql/sql-connection-schemas.ts b/backend/src/services/app-connection/shared/sql/sql-connection-schemas.ts index bfaf6f2bc..8fea2c1fb 100644 --- a/backend/src/services/app-connection/shared/sql/sql-connection-schemas.ts +++ b/backend/src/services/app-connection/shared/sql/sql-connection-schemas.ts @@ -3,7 +3,11 @@ import { z } from "zod"; import { AppConnections } from "@app/lib/api-docs"; export const BaseSqlUsernameAndPasswordConnectionSchema = z.object({ - gatewayId: z.string().optional(), + gatewayId: z + .union([z.string().uuid(), z.literal("")]) + .transform((value) => value || null) + .nullish() + .describe(AppConnections.CREDENTIALS.SQL_CONNECTION.gatewayId), host: z.string().trim().min(1, "Host required").describe(AppConnections.CREDENTIALS.SQL_CONNECTION.host), port: z.coerce.number().describe(AppConnections.CREDENTIALS.SQL_CONNECTION.port), database: z.string().trim().min(1, "Database required").describe(AppConnections.CREDENTIALS.SQL_CONNECTION.database), diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MsSqlConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MsSqlConnectionForm.tsx index 721ff30d4..63ec9f781 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MsSqlConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MsSqlConnectionForm.tsx @@ -152,7 +152,7 @@ export const MsSqlConnectionForm = ({ appConnection, onSubmit }: Props) => {
{
Date: Wed, 16 Jul 2025 03:19:37 -0400 Subject: [PATCH 4/9] Lint fixes --- .../services/secret-rotation-v2/secret-rotation-v2-service.ts | 1 + .../src/hooks/api/appConnections/types/shared/sql-connection.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts index b1cc8ad23..47438ede2 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts @@ -4,6 +4,7 @@ import isEqual from "lodash.isequal"; import { SecretType, TableName } from "@app/db/schemas"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; +import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { hasSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; diff --git a/frontend/src/hooks/api/appConnections/types/shared/sql-connection.ts b/frontend/src/hooks/api/appConnections/types/shared/sql-connection.ts index f88d8f726..fabc7ad80 100644 --- a/frontend/src/hooks/api/appConnections/types/shared/sql-connection.ts +++ b/frontend/src/hooks/api/appConnections/types/shared/sql-connection.ts @@ -1,5 +1,5 @@ export type TBaseSqlConnectionCredentials = { - gatewayId?: string; + gatewayId?: string | null; host: string; port: number; username: string; From a8b448be0f5fd42312ccae03f3760c4c47765b25 Mon Sep 17 00:00:00 2001 From: x032205 Date: Thu, 17 Jul 2025 20:25:10 -0400 Subject: [PATCH 5/9] Swap gateway to outer layer --- .../20250717195959_gatewayid-for-app-conn.ts | 19 ++++++ backend/src/db/schemas/app-connections.ts | 3 +- .../oracledb/oracledb-connection-schemas.ts | 1 - backend/src/lib/api-docs/constants.ts | 1 - backend/src/server/routes/index.ts | 3 +- .../app-connection-endpoints.ts | 13 ++-- .../app-connection/app-connection-schemas.ts | 6 +- .../app-connection/app-connection-service.ts | 59 ++++++++++++++--- .../app-connection/app-connection-types.ts | 2 +- .../mssql/mssql-connection-schemas.ts | 1 - .../mysql/mysql-connection-schemas.ts | 1 - .../postgres/postgres-connection-schemas.ts | 1 - .../shared/sql/sql-connection-fns.ts | 6 +- .../shared/sql/sql-connection-schemas.ts | 5 -- .../shared/sql/sql-connection-types.ts | 5 +- .../hooks/api/appConnections/types/index.ts | 13 +++- .../appConnections/types/root-connection.ts | 1 + .../types/shared/sql-connection.ts | 1 - .../GenericAppConnectionFields.tsx | 3 +- .../AppConnectionForm/MsSqlConnectionForm.tsx | 66 +++++++++---------- .../AppConnectionForm/MySqlConnectionForm.tsx | 66 +++++++++---------- .../OracleDBConnectionForm.tsx | 66 +++++++++---------- .../PostgresConnectionForm.tsx | 66 +++++++++---------- .../shared/sql-connection-schemas.ts | 1 - 24 files changed, 241 insertions(+), 168 deletions(-) create mode 100644 backend/src/db/migrations/20250717195959_gatewayid-for-app-conn.ts diff --git a/backend/src/db/migrations/20250717195959_gatewayid-for-app-conn.ts b/backend/src/db/migrations/20250717195959_gatewayid-for-app-conn.ts new file mode 100644 index 000000000..531cdcae8 --- /dev/null +++ b/backend/src/db/migrations/20250717195959_gatewayid-for-app-conn.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.AppConnection, "gatewayId"))) { + await knex.schema.alterTable(TableName.AppConnection, (t) => { + t.uuid("gatewayId").nullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.AppConnection, "gatewayId")) { + await knex.schema.alterTable(TableName.AppConnection, (t) => { + t.dropColumn("gatewayId"); + }); + } +} diff --git a/backend/src/db/schemas/app-connections.ts b/backend/src/db/schemas/app-connections.ts index ee4282b73..2218b75ce 100644 --- a/backend/src/db/schemas/app-connections.ts +++ b/backend/src/db/schemas/app-connections.ts @@ -20,7 +20,8 @@ export const AppConnectionsSchema = z.object({ orgId: z.string().uuid(), createdAt: z.date(), updatedAt: z.date(), - isPlatformManagedCredentials: z.boolean().default(false).nullable().optional() + isPlatformManagedCredentials: z.boolean().default(false).nullable().optional(), + gatewayId: z.string().uuid().nullable().optional() }); export type TAppConnections = z.infer; diff --git a/backend/src/ee/services/app-connections/oracledb/oracledb-connection-schemas.ts b/backend/src/ee/services/app-connections/oracledb/oracledb-connection-schemas.ts index ed95ba0da..f93abae83 100644 --- a/backend/src/ee/services/app-connections/oracledb/oracledb-connection-schemas.ts +++ b/backend/src/ee/services/app-connections/oracledb/oracledb-connection-schemas.ts @@ -24,7 +24,6 @@ export const SanitizedOracleDBConnectionSchema = z.discriminatedUnion("method", BaseOracleDBConnectionSchema.extend({ method: z.literal(OracleDBConnectionMethod.UsernameAndPassword), credentials: OracleDBConnectionCredentialsSchema.pick({ - gatewayId: true, host: true, database: true, port: true, diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 551b04def..584f480ad 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2199,7 +2199,6 @@ export const AppConnections = { audience: "The unique identifier of the target API you want to access." }, SQL_CONNECTION: { - gatewayId: "The ID of the gateway to use when performing database requests.", host: "The hostname of the database server.", port: "The port number of the database.", database: "The name of the database to connect to.", diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index fbfcd2177..4f7ebeed9 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1707,7 +1707,8 @@ export const registerRoutes = async ( permissionService, kmsService, licenseService, - gatewayService + gatewayService, + gatewayDAL }); const secretSyncService = secretSyncServiceFactory({ diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts index 0bc8f7c59..81f27537d 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts @@ -25,12 +25,14 @@ export const registerAppConnectionEndpoints = ; updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"]; description?: string | null; isPlatformManagedCredentials?: boolean; + gatewayId?: string | null; }>; sanitizedResponseSchema: z.ZodTypeAny; }) => { @@ -224,10 +226,10 @@ export const registerAppConnectionEndpoints = { - const { name, method, credentials, description, isPlatformManagedCredentials } = req.body; + const { name, method, credentials, description, isPlatformManagedCredentials, gatewayId } = req.body; const appConnection = (await server.services.appConnection.createAppConnection( - { name, method, app, credentials, description, isPlatformManagedCredentials }, + { name, method, app, credentials, description, isPlatformManagedCredentials, gatewayId }, req.permission )) as T; @@ -270,11 +272,14 @@ export const registerAppConnectionEndpoints = { - const { name, credentials, description, isPlatformManagedCredentials } = req.body; + const { name, credentials, description, isPlatformManagedCredentials, gatewayId } = req.body; const { connectionId } = req.params; + console.log("123"); + console.log(gatewayId); + const appConnection = (await server.services.appConnection.updateAppConnection( - { name, credentials, connectionId, description, isPlatformManagedCredentials }, + { name, credentials, connectionId, description, isPlatformManagedCredentials, gatewayId }, req.permission )) as T; diff --git a/backend/src/services/app-connection/app-connection-schemas.ts b/backend/src/services/app-connection/app-connection-schemas.ts index 0d3968637..a2839d2ba 100644 --- a/backend/src/services/app-connection/app-connection-schemas.ts +++ b/backend/src/services/app-connection/app-connection-schemas.ts @@ -30,7 +30,8 @@ export const GenericCreateAppConnectionFieldsSchema = ( .describe(AppConnections.CREATE(app).description), isPlatformManagedCredentials: supportsPlatformManagedCredentials ? z.boolean().optional().default(false).describe(AppConnections.CREATE(app).isPlatformManagedCredentials) - : z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`) + : z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`), + gatewayId: z.string().uuid().nullish().describe("The Gateway ID to use for this connection.") }); export const GenericUpdateAppConnectionFieldsSchema = ( @@ -47,5 +48,6 @@ export const GenericUpdateAppConnectionFieldsSchema = ( .describe(AppConnections.UPDATE(app).description), isPlatformManagedCredentials: supportsPlatformManagedCredentials ? z.boolean().optional().describe(AppConnections.UPDATE(app).isPlatformManagedCredentials) - : z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`) + : z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`), + gatewayId: z.string().uuid().nullish().describe("The Gateway ID to use for this connection.") }); diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index ebe0cf4d1..bfff562d7 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -3,9 +3,14 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci"; import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service"; import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb"; +import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { + OrgPermissionAppConnectionActions, + OrgPermissionGatewayActions, + OrgPermissionSubjects +} from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { crypto } from "@app/lib/crypto/cryptography"; import { DatabaseErrorCode } from "@app/lib/error-codes"; @@ -98,6 +103,7 @@ export type TAppConnectionServiceFactoryDep = { kmsService: Pick; licenseService: Pick; gatewayService: Pick; + gatewayDAL: Pick; }; export type TAppConnectionServiceFactory = ReturnType; @@ -144,7 +150,8 @@ export const appConnectionServiceFactory = ({ permissionService, kmsService, licenseService, - gatewayService + gatewayService, + gatewayDAL }: TAppConnectionServiceFactoryDep) => { const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => { const { permission } = await permissionService.getOrgPermission( @@ -225,7 +232,7 @@ export const appConnectionServiceFactory = ({ }; const createAppConnection = async ( - { method, app, credentials, ...params }: TCreateAppConnectionDTO, + { method, app, credentials, gatewayId, ...params }: TCreateAppConnectionDTO, actor: OrgServiceActor ) => { const { permission } = await permissionService.getOrgPermission( @@ -241,6 +248,20 @@ export const appConnectionServiceFactory = ({ OrgPermissionSubjects.AppConnections ); + if (gatewayId) { + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionGatewayActions.AttachGateways, + OrgPermissionSubjects.Gateway + ); + + const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actor.orgId }); + if (!gateway) { + throw new NotFoundError({ + message: `Gateway with ID ${gatewayId} not found for org` + }); + } + } + await enterpriseAppCheck( licenseService, app, @@ -253,7 +274,8 @@ export const appConnectionServiceFactory = ({ app, credentials, method, - orgId: actor.orgId + orgId: actor.orgId, + gatewayId } as TAppConnectionConfig, gatewayService ); @@ -271,6 +293,7 @@ export const appConnectionServiceFactory = ({ encryptedCredentials, method, app, + gatewayId, ...params }); }; @@ -283,7 +306,8 @@ export const appConnectionServiceFactory = ({ app, orgId: actor.orgId, credentials: validatedCredentials, - method + method, + gatewayId } as TAppConnectionConfig, (platformCredentials) => createConnection(platformCredentials), gatewayService @@ -307,7 +331,7 @@ export const appConnectionServiceFactory = ({ }; const updateAppConnection = async ( - { connectionId, credentials, ...params }: TUpdateAppConnectionDTO, + { connectionId, credentials, gatewayId, ...params }: TUpdateAppConnectionDTO, actor: OrgServiceActor ) => { const appConnection = await appConnectionDAL.findById(connectionId); @@ -334,6 +358,22 @@ export const appConnectionServiceFactory = ({ OrgPermissionSubjects.AppConnections ); + if (gatewayId !== appConnection.gatewayId) { + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionGatewayActions.AttachGateways, + OrgPermissionSubjects.Gateway + ); + + if (gatewayId) { + const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actor.orgId }); + if (!gateway) { + throw new NotFoundError({ + message: `Gateway with ID ${gatewayId} not found for org` + }); + } + } + } + // prevent updating credentials or management status if platform managed if (appConnection.isPlatformManagedCredentials && (params.isPlatformManagedCredentials === false || credentials)) { throw new BadRequestError({ @@ -363,7 +403,8 @@ export const appConnectionServiceFactory = ({ app, orgId: actor.orgId, credentials, - method + method, + gatewayId } as TAppConnectionConfig, gatewayService ); @@ -385,6 +426,7 @@ export const appConnectionServiceFactory = ({ return appConnectionDAL.updateById(connectionId, { orgId: actor.orgId, encryptedCredentials, + gatewayId, ...params }); }; @@ -401,7 +443,8 @@ export const appConnectionServiceFactory = ({ app, orgId: actor.orgId, credentials: updatedCredentials, - method + method, + gatewayId } as TAppConnectionConfig, (platformCredentials) => updateConnection(platformCredentials), gatewayService diff --git a/backend/src/services/app-connection/app-connection-types.ts b/backend/src/services/app-connection/app-connection-types.ts index a53b3dde2..519431ce8 100644 --- a/backend/src/services/app-connection/app-connection-types.ts +++ b/backend/src/services/app-connection/app-connection-types.ts @@ -283,7 +283,7 @@ export type TSqlConnectionInput = export type TCreateAppConnectionDTO = Pick< TAppConnectionInput, - "credentials" | "method" | "name" | "app" | "description" | "isPlatformManagedCredentials" + "credentials" | "method" | "name" | "app" | "description" | "isPlatformManagedCredentials" | "gatewayId" >; export type TUpdateAppConnectionDTO = Partial> & { diff --git a/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts b/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts index e48b42286..994f9a40d 100644 --- a/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts +++ b/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts @@ -26,7 +26,6 @@ export const SanitizedMsSqlConnectionSchema = z.discriminatedUnion("method", [ BaseMsSqlConnectionSchema.extend({ method: z.literal(MsSqlConnectionMethod.UsernameAndPassword), credentials: MsSqlConnectionAccessTokenCredentialsSchema.pick({ - gatewayId: true, host: true, database: true, port: true, diff --git a/backend/src/services/app-connection/mysql/mysql-connection-schemas.ts b/backend/src/services/app-connection/mysql/mysql-connection-schemas.ts index 0196f2f02..082bac557 100644 --- a/backend/src/services/app-connection/mysql/mysql-connection-schemas.ts +++ b/backend/src/services/app-connection/mysql/mysql-connection-schemas.ts @@ -24,7 +24,6 @@ export const SanitizedMySqlConnectionSchema = z.discriminatedUnion("method", [ BaseMySqlConnectionSchema.extend({ method: z.literal(MySqlConnectionMethod.UsernameAndPassword), credentials: MySqlConnectionAccessTokenCredentialsSchema.pick({ - gatewayId: true, host: true, database: true, port: true, diff --git a/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts b/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts index a9edf7710..1ddf1e2da 100644 --- a/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts +++ b/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts @@ -24,7 +24,6 @@ export const SanitizedPostgresConnectionSchema = z.discriminatedUnion("method", BasePostgresConnectionSchema.extend({ method: z.literal(PostgresConnectionMethod.UsernameAndPassword), credentials: PostgresConnectionAccessTokenCredentialsSchema.pick({ - gatewayId: true, host: true, database: true, port: true, diff --git a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts index 49204e9e4..d9adc91dd 100644 --- a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts +++ b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts @@ -105,11 +105,11 @@ export const executeWithPotentialGateway = async ( gatewayService: Pick, operation: (client: Knex) => Promise ): Promise => { - const { credentials, app } = config; + const { credentials, app, gatewayId } = config; - if (credentials.gatewayId && gatewayService) { + if (gatewayId && gatewayService) { const [targetHost] = await verifyHostInputValidity(credentials.host, true); - const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(credentials.gatewayId); + const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId); const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); return withGatewayProxy( diff --git a/backend/src/services/app-connection/shared/sql/sql-connection-schemas.ts b/backend/src/services/app-connection/shared/sql/sql-connection-schemas.ts index 8fea2c1fb..500ed596a 100644 --- a/backend/src/services/app-connection/shared/sql/sql-connection-schemas.ts +++ b/backend/src/services/app-connection/shared/sql/sql-connection-schemas.ts @@ -3,11 +3,6 @@ import { z } from "zod"; import { AppConnections } from "@app/lib/api-docs"; export const BaseSqlUsernameAndPasswordConnectionSchema = z.object({ - gatewayId: z - .union([z.string().uuid(), z.literal("")]) - .transform((value) => value || null) - .nullish() - .describe(AppConnections.CREDENTIALS.SQL_CONNECTION.gatewayId), host: z.string().trim().min(1, "Host required").describe(AppConnections.CREDENTIALS.SQL_CONNECTION.host), port: z.coerce.number().describe(AppConnections.CREDENTIALS.SQL_CONNECTION.port), database: z.string().trim().min(1, "Database required").describe(AppConnections.CREDENTIALS.SQL_CONNECTION.database), diff --git a/backend/src/services/app-connection/shared/sql/sql-connection-types.ts b/backend/src/services/app-connection/shared/sql/sql-connection-types.ts index bbfe4086c..104aacfb9 100644 --- a/backend/src/services/app-connection/shared/sql/sql-connection-types.ts +++ b/backend/src/services/app-connection/shared/sql/sql-connection-types.ts @@ -1,6 +1,9 @@ import { DiscriminativePick } from "@app/lib/types"; import { TSqlConnectionInput } from "@app/services/app-connection/app-connection-types"; -export type TSqlConnectionConfig = DiscriminativePick & { +export type TSqlConnectionConfig = DiscriminativePick< + TSqlConnectionInput, + "method" | "app" | "credentials" | "gatewayId" +> & { orgId: string; }; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index e177fa2ab..3fc659793 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -113,11 +113,20 @@ export type TAvailableAppConnectionsResponse = { appConnections: TAvailableAppCo export type TCreateAppConnectionDTO = Pick< TAppConnection, - "name" | "credentials" | "method" | "app" | "description" | "isPlatformManagedCredentials" + | "name" + | "credentials" + | "method" + | "app" + | "description" + | "isPlatformManagedCredentials" + | "gatewayId" >; export type TUpdateAppConnectionDTO = Partial< - Pick + Pick< + TAppConnection, + "name" | "credentials" | "description" | "isPlatformManagedCredentials" | "gatewayId" + > > & { connectionId: string; app: AppConnection; diff --git a/frontend/src/hooks/api/appConnections/types/root-connection.ts b/frontend/src/hooks/api/appConnections/types/root-connection.ts index 52571d067..5b8f5cd02 100644 --- a/frontend/src/hooks/api/appConnections/types/root-connection.ts +++ b/frontend/src/hooks/api/appConnections/types/root-connection.ts @@ -7,4 +7,5 @@ export type TRootAppConnection = { createdAt: string; updatedAt: string; isPlatformManagedCredentials?: boolean; + gatewayId?: string | null; }; diff --git a/frontend/src/hooks/api/appConnections/types/shared/sql-connection.ts b/frontend/src/hooks/api/appConnections/types/shared/sql-connection.ts index fabc7ad80..d0e71158e 100644 --- a/frontend/src/hooks/api/appConnections/types/shared/sql-connection.ts +++ b/frontend/src/hooks/api/appConnections/types/shared/sql-connection.ts @@ -1,5 +1,4 @@ export type TBaseSqlConnectionCredentials = { - gatewayId?: string | null; host: string; port: number; username: string; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GenericAppConnectionFields.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GenericAppConnectionFields.tsx index 70128025d..4c1e3b4aa 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GenericAppConnectionFields.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GenericAppConnectionFields.tsx @@ -6,7 +6,8 @@ import { slugSchema } from "@app/lib/schemas"; export const genericAppConnectionFieldsSchema = z.object({ name: slugSchema({ min: 1, max: 64, field: "Name" }), - description: z.string().trim().max(256, "Description cannot exceed 256 characters").nullish() + description: z.string().trim().max(256, "Description cannot exceed 256 characters").nullish(), + gatewayId: z.string().uuid().nullish() }); export const GenericAppConnectionsFields = () => { diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MsSqlConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MsSqlConnectionForm.tsx index 63ec9f781..0735a863c 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MsSqlConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MsSqlConnectionForm.tsx @@ -58,8 +58,8 @@ export const MsSqlConnectionForm = ({ appConnection, onSubmit }: Props) => { defaultValues: appConnection ?? { app: AppConnection.MsSql, method: MsSqlConnectionMethod.UsernameAndPassword, + gatewayId: null, credentials: { - gatewayId: "", host: "", port: 1433, database: "default", @@ -99,37 +99,6 @@ export const MsSqlConnectionForm = ({ appConnection, onSubmit }: Props) => { }} > {!isUpdate && } - ( - - - - )} - /> { {(isAllowed) => ( ( { /> )} + ( + + + + )} + /> { defaultValues: appConnection ?? { app: AppConnection.MySql, method: MySqlConnectionMethod.UsernameAndPassword, + gatewayId: null, credentials: { - gatewayId: "", host: "", port: 3306, database: "default", @@ -96,37 +96,6 @@ export const MySqlConnectionForm = ({ appConnection, onSubmit }: Props) => { }} > {!isUpdate && } - ( - - - - )} - /> { {(isAllowed) => ( ( { /> )} + ( + + + + )} + /> { defaultValues: appConnection ?? { app: AppConnection.OracleDB, method: OracleDBConnectionMethod.UsernameAndPassword, + gatewayId: null, credentials: { - gatewayId: "", host: "", port: 1521, database: "ORCL", // Typically FREEPDB1 or ORCL @@ -96,37 +96,6 @@ export const OracleDBConnectionForm = ({ appConnection, onSubmit }: Props) => { }} > {!isUpdate && } - ( - - - - )} - /> { {(isAllowed) => ( ( { /> )} + ( + + + + )} + /> { defaultValues: appConnection ?? { app: AppConnection.Postgres, method: PostgresConnectionMethod.UsernameAndPassword, + gatewayId: null, credentials: { - gatewayId: "", host: "", port: 5432, database: "default", @@ -96,37 +96,6 @@ export const PostgresConnectionForm = ({ appConnection, onSubmit }: Props) => { }} > {!isUpdate && } - ( - - - - )} - /> { {(isAllowed) => ( ( { /> )} + ( + + + + )} + /> Date: Thu, 17 Jul 2025 20:29:48 -0400 Subject: [PATCH 6/9] Remove console log --- .../v1/app-connection-routers/app-connection-endpoints.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts index 81f27537d..50111b109 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts @@ -275,9 +275,6 @@ export const registerAppConnectionEndpoints = Date: Fri, 18 Jul 2025 14:42:19 -0400 Subject: [PATCH 7/9] Only allow gateway for supported connections --- .../oracledb/oracledb-connection-schemas.ts | 12 ++++++++-- .../app-connection/app-connection-schemas.ts | 22 +++++++++++++++---- .../app-connection/app-connection-types.ts | 1 + .../mssql/mssql-connection-schemas.ts | 12 ++++++++-- .../mysql/mysql-connection-schemas.ts | 12 ++++++++-- .../postgres/postgres-connection-schemas.ts | 12 ++++++++-- .../GenericAppConnectionFields.tsx | 2 +- 7 files changed, 60 insertions(+), 13 deletions(-) diff --git a/backend/src/ee/services/app-connections/oracledb/oracledb-connection-schemas.ts b/backend/src/ee/services/app-connections/oracledb/oracledb-connection-schemas.ts index f93abae83..38e0fc828 100644 --- a/backend/src/ee/services/app-connections/oracledb/oracledb-connection-schemas.ts +++ b/backend/src/ee/services/app-connections/oracledb/oracledb-connection-schemas.ts @@ -45,7 +45,10 @@ export const ValidateOracleDBConnectionCredentialsSchema = z.discriminatedUnion( ]); export const CreateOracleDBConnectionSchema = ValidateOracleDBConnectionCredentialsSchema.and( - GenericCreateAppConnectionFieldsSchema(AppConnection.OracleDB, { supportsPlatformManagedCredentials: true }) + GenericCreateAppConnectionFieldsSchema(AppConnection.OracleDB, { + supportsPlatformManagedCredentials: true, + supportsGateways: true + }) ); export const UpdateOracleDBConnectionSchema = z @@ -54,7 +57,12 @@ export const UpdateOracleDBConnectionSchema = z AppConnections.UPDATE(AppConnection.OracleDB).credentials ) }) - .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.OracleDB, { supportsPlatformManagedCredentials: true })); + .and( + GenericUpdateAppConnectionFieldsSchema(AppConnection.OracleDB, { + supportsPlatformManagedCredentials: true, + supportsGateways: true + }) + ); export const OracleDBConnectionListItemSchema = z.object({ name: z.literal("OracleDB"), diff --git a/backend/src/services/app-connection/app-connection-schemas.ts b/backend/src/services/app-connection/app-connection-schemas.ts index a2839d2ba..de9b4f546 100644 --- a/backend/src/services/app-connection/app-connection-schemas.ts +++ b/backend/src/services/app-connection/app-connection-schemas.ts @@ -18,7 +18,7 @@ export const BaseAppConnectionSchema = AppConnectionsSchema.omit({ export const GenericCreateAppConnectionFieldsSchema = ( app: AppConnection, - { supportsPlatformManagedCredentials = false }: TAppConnectionBaseConfig = {} + { supportsPlatformManagedCredentials = false, supportsGateways = false }: TAppConnectionBaseConfig = {} ) => z.object({ name: slugSchema({ field: "name" }).describe(AppConnections.CREATE(app).name), @@ -31,12 +31,19 @@ export const GenericCreateAppConnectionFieldsSchema = ( isPlatformManagedCredentials: supportsPlatformManagedCredentials ? z.boolean().optional().default(false).describe(AppConnections.CREATE(app).isPlatformManagedCredentials) : z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`), - gatewayId: z.string().uuid().nullish().describe("The Gateway ID to use for this connection.") + gatewayId: supportsGateways + ? z + .preprocess((val) => (val === "" ? null : val), z.string().uuid().nullish()) + .describe("The Gateway ID to use for this connection.") + : z + .union([z.literal(undefined), z.literal("")]) + .transform((v) => (v === "" ? undefined : v)) + .describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`) }); export const GenericUpdateAppConnectionFieldsSchema = ( app: AppConnection, - { supportsPlatformManagedCredentials = false }: TAppConnectionBaseConfig = {} + { supportsPlatformManagedCredentials = false, supportsGateways = false }: TAppConnectionBaseConfig = {} ) => z.object({ name: slugSchema({ field: "name" }).describe(AppConnections.UPDATE(app).name).optional(), @@ -49,5 +56,12 @@ export const GenericUpdateAppConnectionFieldsSchema = ( isPlatformManagedCredentials: supportsPlatformManagedCredentials ? z.boolean().optional().describe(AppConnections.UPDATE(app).isPlatformManagedCredentials) : z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`), - gatewayId: z.string().uuid().nullish().describe("The Gateway ID to use for this connection.") + gatewayId: supportsGateways + ? z + .preprocess((val) => (val === "" ? null : val), z.string().uuid().nullish()) + .describe("The Gateway ID to use for this connection.") + : z + .union([z.literal(undefined), z.literal("")]) + .transform((v) => (v === "" ? undefined : v)) + .describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`) }); diff --git a/backend/src/services/app-connection/app-connection-types.ts b/backend/src/services/app-connection/app-connection-types.ts index 519431ce8..077909730 100644 --- a/backend/src/services/app-connection/app-connection-types.ts +++ b/backend/src/services/app-connection/app-connection-types.ts @@ -382,4 +382,5 @@ export type TAppConnectionTransitionCredentialsToPlatform = ( export type TAppConnectionBaseConfig = { supportsPlatformManagedCredentials?: boolean; + supportsGateways?: boolean; }; diff --git a/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts b/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts index 994f9a40d..f8d380949 100644 --- a/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts +++ b/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts @@ -49,7 +49,10 @@ export const ValidateMsSqlConnectionCredentialsSchema = z.discriminatedUnion("me ]); export const CreateMsSqlConnectionSchema = ValidateMsSqlConnectionCredentialsSchema.and( - GenericCreateAppConnectionFieldsSchema(AppConnection.MsSql, { supportsPlatformManagedCredentials: true }) + GenericCreateAppConnectionFieldsSchema(AppConnection.MsSql, { + supportsPlatformManagedCredentials: true, + supportsGateways: true + }) ); export const UpdateMsSqlConnectionSchema = z @@ -58,7 +61,12 @@ export const UpdateMsSqlConnectionSchema = z AppConnections.UPDATE(AppConnection.MsSql).credentials ) }) - .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.MsSql, { supportsPlatformManagedCredentials: true })); + .and( + GenericUpdateAppConnectionFieldsSchema(AppConnection.MsSql, { + supportsPlatformManagedCredentials: true, + supportsGateways: true + }) + ); export const MsSqlConnectionListItemSchema = z.object({ name: z.literal("Microsoft SQL Server"), diff --git a/backend/src/services/app-connection/mysql/mysql-connection-schemas.ts b/backend/src/services/app-connection/mysql/mysql-connection-schemas.ts index 082bac557..51a533395 100644 --- a/backend/src/services/app-connection/mysql/mysql-connection-schemas.ts +++ b/backend/src/services/app-connection/mysql/mysql-connection-schemas.ts @@ -47,7 +47,10 @@ export const ValidateMySqlConnectionCredentialsSchema = z.discriminatedUnion("me ]); export const CreateMySqlConnectionSchema = ValidateMySqlConnectionCredentialsSchema.and( - GenericCreateAppConnectionFieldsSchema(AppConnection.MySql, { supportsPlatformManagedCredentials: true }) + GenericCreateAppConnectionFieldsSchema(AppConnection.MySql, { + supportsPlatformManagedCredentials: true, + supportsGateways: true + }) ); export const UpdateMySqlConnectionSchema = z @@ -56,7 +59,12 @@ export const UpdateMySqlConnectionSchema = z AppConnections.UPDATE(AppConnection.MySql).credentials ) }) - .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.MySql, { supportsPlatformManagedCredentials: true })); + .and( + GenericUpdateAppConnectionFieldsSchema(AppConnection.MySql, { + supportsPlatformManagedCredentials: true, + supportsGateways: true + }) + ); export const MySqlConnectionListItemSchema = z.object({ name: z.literal("MySQL"), diff --git a/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts b/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts index 1ddf1e2da..da74bd669 100644 --- a/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts +++ b/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts @@ -47,7 +47,10 @@ export const ValidatePostgresConnectionCredentialsSchema = z.discriminatedUnion( ]); export const CreatePostgresConnectionSchema = ValidatePostgresConnectionCredentialsSchema.and( - GenericCreateAppConnectionFieldsSchema(AppConnection.Postgres, { supportsPlatformManagedCredentials: true }) + GenericCreateAppConnectionFieldsSchema(AppConnection.Postgres, { + supportsPlatformManagedCredentials: true, + supportsGateways: true + }) ); export const UpdatePostgresConnectionSchema = z @@ -56,7 +59,12 @@ export const UpdatePostgresConnectionSchema = z AppConnections.UPDATE(AppConnection.Postgres).credentials ) }) - .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Postgres, { supportsPlatformManagedCredentials: true })); + .and( + GenericUpdateAppConnectionFieldsSchema(AppConnection.Postgres, { + supportsPlatformManagedCredentials: true, + supportsGateways: true + }) + ); export const PostgresConnectionListItemSchema = z.object({ name: z.literal("PostgreSQL"), diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GenericAppConnectionFields.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GenericAppConnectionFields.tsx index 4c1e3b4aa..6508b28d0 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GenericAppConnectionFields.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GenericAppConnectionFields.tsx @@ -7,7 +7,7 @@ import { slugSchema } from "@app/lib/schemas"; export const genericAppConnectionFieldsSchema = z.object({ name: slugSchema({ min: 1, max: 64, field: "Name" }), description: z.string().trim().max(256, "Description cannot exceed 256 characters").nullish(), - gatewayId: z.string().uuid().nullish() + gatewayId: z.string().nullish() }); export const GenericAppConnectionsFields = () => { From 4ab0da6b033717583dce8faab5093ebe15e9708f Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 18 Jul 2025 16:22:51 -0400 Subject: [PATCH 8/9] Fix type stuff --- .../app-connection/app-connection-schemas.ts | 32 +++++++++++-------- .../GenericAppConnectionFields.tsx | 5 ++- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/backend/src/services/app-connection/app-connection-schemas.ts b/backend/src/services/app-connection/app-connection-schemas.ts index de9b4f546..92fc83123 100644 --- a/backend/src/services/app-connection/app-connection-schemas.ts +++ b/backend/src/services/app-connection/app-connection-schemas.ts @@ -30,14 +30,16 @@ export const GenericCreateAppConnectionFieldsSchema = ( .describe(AppConnections.CREATE(app).description), isPlatformManagedCredentials: supportsPlatformManagedCredentials ? z.boolean().optional().default(false).describe(AppConnections.CREATE(app).isPlatformManagedCredentials) - : z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`), - gatewayId: supportsGateways - ? z - .preprocess((val) => (val === "" ? null : val), z.string().uuid().nullish()) - .describe("The Gateway ID to use for this connection.") : z - .union([z.literal(undefined), z.literal("")]) - .transform((v) => (v === "" ? undefined : v)) + .literal(false, { + errorMap: () => ({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` }) + }) + .optional() + .describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`), + gatewayId: supportsGateways + ? z.string().uuid().nullish().describe("The Gateway ID to use for this connection.") + : z + .undefined({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` }) .describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`) }); @@ -55,13 +57,15 @@ export const GenericUpdateAppConnectionFieldsSchema = ( .describe(AppConnections.UPDATE(app).description), isPlatformManagedCredentials: supportsPlatformManagedCredentials ? z.boolean().optional().describe(AppConnections.UPDATE(app).isPlatformManagedCredentials) - : z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`), - gatewayId: supportsGateways - ? z - .preprocess((val) => (val === "" ? null : val), z.string().uuid().nullish()) - .describe("The Gateway ID to use for this connection.") : z - .union([z.literal(undefined), z.literal("")]) - .transform((v) => (v === "" ? undefined : v)) + .literal(false, { + errorMap: () => ({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` }) + }) + .optional() + .describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`), + gatewayId: supportsGateways + ? z.string().uuid().nullish().describe("The Gateway ID to use for this connection.") + : z + .undefined({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` }) .describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`) }); diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GenericAppConnectionFields.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GenericAppConnectionFields.tsx index 6508b28d0..e7d9cf80c 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GenericAppConnectionFields.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GenericAppConnectionFields.tsx @@ -7,7 +7,10 @@ import { slugSchema } from "@app/lib/schemas"; export const genericAppConnectionFieldsSchema = z.object({ name: slugSchema({ min: 1, max: 64, field: "Name" }), description: z.string().trim().max(256, "Description cannot exceed 256 characters").nullish(), - gatewayId: z.string().nullish() + gatewayId: z + .string() + .nullish() + .transform((v) => (v === "" ? null : v)) }); export const GenericAppConnectionsFields = () => { From f3a8e305482b30d06249a66d7925a586abdf766c Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Fri, 18 Jul 2025 13:40:42 -0700 Subject: [PATCH 9/9] improvement: allow null for non-supported gatewayId --- backend/src/services/app-connection/app-connection-schemas.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/services/app-connection/app-connection-schemas.ts b/backend/src/services/app-connection/app-connection-schemas.ts index 92fc83123..d0dcb1a54 100644 --- a/backend/src/services/app-connection/app-connection-schemas.ts +++ b/backend/src/services/app-connection/app-connection-schemas.ts @@ -40,6 +40,7 @@ export const GenericCreateAppConnectionFieldsSchema = ( ? z.string().uuid().nullish().describe("The Gateway ID to use for this connection.") : z .undefined({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` }) + .or(z.null({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` })) .describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`) }); @@ -67,5 +68,6 @@ export const GenericUpdateAppConnectionFieldsSchema = ( ? z.string().uuid().nullish().describe("The Gateway ID to use for this connection.") : z .undefined({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` }) + .or(z.null({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` })) .describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`) });