From a0595fa9b4ba2265049566d76a6330545cce3858 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 10 Sep 2025 19:59:29 +0800 Subject: [PATCH 1/3] fix: address sql server dynamic secret integration --- .../dynamic-secret/providers/models.ts | 1 + .../dynamic-secret/providers/sql-database.ts | 37 ++++++++++++++++--- .../SqlDatabaseInputForm.tsx | 26 ++++++++++++- .../EditDynamicSecretSqlProviderForm.tsx | 26 ++++++++++++- 4 files changed, 83 insertions(+), 7 deletions(-) diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index ae1bcfc25..c618a308b 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -165,6 +165,7 @@ export const DynamicSecretSqlDBSchema = z.object({ revocationStatement: z.string().trim(), renewStatement: z.string().trim().optional(), ca: z.string().optional(), + sslEnabled: z.boolean().optional(), gatewayId: z.string().nullable().optional() }); diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts index 57ce710b1..babbe1912 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -1,4 +1,5 @@ import handlebars from "handlebars"; +import RE2 from "re2"; import knex from "knex"; import { z } from "zod"; @@ -154,7 +155,15 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca, servername: providerInputs.host } : undefined; + const isMsSQLClient = providerInputs.client === SqlProviders.MsSQL; + const isAzureSql = isMsSQLClient && new RE2(/\.database\.windows\.net$/i).test(providerInputs.host); + const azureServerLabel = isAzureSql ? providerInputs.host.split(".")[0] : undefined; + + const effectiveUser = + isAzureSql && !providerInputs.username.includes("@") + ? `${providerInputs.username}@${azureServerLabel}` + : providerInputs.username; const db = knex({ client: providerInputs.client, @@ -162,7 +171,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) database: providerInputs.database, port: providerInputs.port, host: providerInputs.client === SqlProviders.Postgres ? providerInputs.hostIp : providerInputs.host, - user: providerInputs.username, + user: effectiveUser, password: providerInputs.password, ssl, // @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver @@ -170,6 +179,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) // https://github.com/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19 options: isMsSQLClient ? { + ...(providerInputs.sslEnabled !== undefined ? { encrypt: providerInputs.sslEnabled } : {}), trustServerCertificate: !providerInputs.ca, cryptoCredentialsDetails: providerInputs.ca ? { ca: providerInputs.ca } : {} } @@ -212,7 +222,12 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) const providerInputs = await validateProviderInputs(inputs); let isConnected = false; const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => { - const db = await $getClient({ ...providerInputs, port, host, hostIp: providerInputs.hostIp }); + const db = await $getClient({ + ...providerInputs, + port, + host, + hostIp: providerInputs.hostIp + }); // oracle needs from keyword const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1"; @@ -253,7 +268,11 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements); const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => { - const db = await $getClient({ ...providerInputs, port, host }); + const db = await $getClient({ + ...providerInputs, + port, + host + }); try { const expiration = new Date(expireAt).toISOString(); @@ -296,7 +315,11 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) const username = entityId; const { database } = providerInputs; const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => { - const db = await $getClient({ ...providerInputs, port, host }); + const db = await $getClient({ + ...providerInputs, + port, + host + }); try { const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database }); const queries = revokeStatement.toString().split(";").filter(Boolean); @@ -331,7 +354,11 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) if (!providerInputs.renewStatement) return { entityId }; const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => { - const db = await $getClient({ ...providerInputs, port, host }); + const db = await $getClient({ + ...providerInputs, + port, + host + }); const expiration = new Date(expireAt).toISOString(); const { database } = providerInputs; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx index 8bedbbaeb..050a51754 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx @@ -19,6 +19,7 @@ import { SecretInput, Select, SelectItem, + Switch, TextArea, Tooltip } from "@app/components/v2"; @@ -66,6 +67,7 @@ const formSchema = z.object({ creationStatement: z.string().min(1), revocationStatement: z.string().min(1), renewStatement: z.string().optional(), + sslEnabled: z.boolean().optional(), ca: z.string().optional(), gatewayId: z.string().optional() }), @@ -200,6 +202,7 @@ export const SqlDatabaseInputForm = ({ const createDynamicSecret = useCreateDynamicSecret(); const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); + const selectedClient = watch("provider.client"); const handleCreateDynamicSecret = async ({ name, @@ -458,13 +461,34 @@ export const SqlDatabaseInputForm = ({ />
+ {selectedClient === SqlProviders.MsSQL && ( +
+ ( + + + Encrypt Connection (SSL) + + + )} + /> +
+ )} ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx index 2cbad1e19..1eda17bc8 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx @@ -18,6 +18,7 @@ import { SecretInput, Select, SelectItem, + Switch, TextArea, Tooltip } from "@app/components/v2"; @@ -63,6 +64,7 @@ const formSchema = z.object({ creationStatement: z.string().min(1), revocationStatement: z.string().min(1), renewStatement: z.string().optional(), + sslEnabled: z.boolean().optional(), ca: z.string().optional(), gatewayId: z.string().optional().nullable() }) @@ -151,6 +153,7 @@ export const EditDynamicSecretSqlProviderForm = ({ }); const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); + const selectedClient = watch("inputs.client"); const updateDynamicSecret = useUpdateDynamicSecret(); const selectedGatewayId = watch("inputs.gatewayId"); @@ -407,13 +410,34 @@ export const EditDynamicSecretSqlProviderForm = ({ />
+ {selectedClient === SqlProviders.MsSQL && ( +
+ ( + + + Encrypt Connection (SSL) + + + )} + /> +
+ )} ( From 7ea0cc6cb7e41bd19c0e5149a5c0ea0a4f6eb4c1 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 10 Sep 2025 20:47:14 +0800 Subject: [PATCH 2/3] fix: passed original host and added context comment --- .../dynamic-secret/providers/sql-database.ts | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts index babbe1912..562aa58f2 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -151,15 +151,26 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) return { ...providerInputs, hostIp }; }; - const $getClient = async (providerInputs: z.infer & { hostIp: string }) => { + const $getClient = async ( + providerInputs: z.infer & { hostIp: string; originalHost: string } + ) => { const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca, servername: providerInputs.host } : undefined; const isMsSQLClient = providerInputs.client === SqlProviders.MsSQL; - const isAzureSql = isMsSQLClient && new RE2(/\.database\.windows\.net$/i).test(providerInputs.host); - const azureServerLabel = isAzureSql ? providerInputs.host.split(".")[0] : undefined; + /* + We route through the gateway by setting connection.host = "localhost". + Azure SQL identifies the logical server from the TDS login name when the host + isn’t the Azure FQDN. Therefore, when using the gateway, ensure username is + "user@" so Azure opens the correct logical server. + Direct connections to the Azure FQDN usually don’t require this suffix. + */ + const isGatewayForwardedTraffic = providerInputs.host === "localhost"; + const isAzureSql = isMsSQLClient && new RE2(/\.database\.windows\.net$/i).test(providerInputs.originalHost); + const azureServerLabel = + isAzureSql && isGatewayForwardedTraffic ? providerInputs.originalHost?.split(".")[0] : undefined; const effectiveUser = isAzureSql && !providerInputs.username.includes("@") ? `${providerInputs.username}@${azureServerLabel}` @@ -226,7 +237,8 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) ...providerInputs, port, host, - hostIp: providerInputs.hostIp + hostIp: providerInputs.hostIp, + originalHost: providerInputs.host }); // oracle needs from keyword const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1"; @@ -271,7 +283,8 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) const db = await $getClient({ ...providerInputs, port, - host + host, + originalHost: providerInputs.host }); try { const expiration = new Date(expireAt).toISOString(); @@ -318,7 +331,8 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) const db = await $getClient({ ...providerInputs, port, - host + host, + originalHost: providerInputs.host }); try { const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database }); @@ -357,7 +371,8 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) const db = await $getClient({ ...providerInputs, port, - host + host, + originalHost: providerInputs.host }); const expiration = new Date(expireAt).toISOString(); const { database } = providerInputs; From a2a96d41cfc94f45c35c7c09911847b86aa70b29 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 10 Sep 2025 21:41:03 +0800 Subject: [PATCH 3/3] misc: added azure server label condition --- .../src/ee/services/dynamic-secret/providers/sql-database.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts index e2b89ac04..2b9636724 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -171,7 +171,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) const azureServerLabel = isAzureSql && providerInputs.gatewayId ? providerInputs.originalHost?.split(".")[0] : undefined; const effectiveUser = - isAzureSql && !providerInputs.username.includes("@") + isAzureSql && !providerInputs.username.includes("@") && azureServerLabel ? `${providerInputs.username}@${azureServerLabel}` : providerInputs.username;