feat(app-connection): Gateway support for SQL connections

This commit is contained in:
x032205
2025-07-16 01:33:22 -04:00
parent 7ea8c74a0d
commit aab204a68a
15 changed files with 333 additions and 30 deletions
@@ -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,
+2 -1
View File
@@ -1706,7 +1706,8 @@ export const registerRoutes = async (
appConnectionDAL,
permissionService,
kmsService,
licenseService
licenseService,
gatewayService
});
const secretSyncService = secretSyncServiceFactory({
@@ -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<TPermissionServiceFactory, "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
};
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
@@ -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);
@@ -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<TAppConnectionRaw>
callback: (credentials: TAppConnection["credentials"]) => Promise<TAppConnectionRaw>,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => Promise<TAppConnectionRaw>;
export type TAppConnectionBaseConfig = {
@@ -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,
@@ -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,
@@ -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,
@@ -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<TSqlConnection,
return client;
};
export const validateSqlConnectionCredentials = async (config: TSqlConnectionConfig) => {
const executeWithPotentialGateway = async <T>(
config: TSqlConnectionConfig,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
operation: (client: Knex) => Promise<T>
): Promise<T> => {
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<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
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<TAppConnectionRaw>
callback: (credentials: TSqlConnectionConfig["credentials"]) => Promise<TAppConnectionRaw>,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
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();
}
};
@@ -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),