Merge pull request #4512 from Infisical/fix/address-azure-sql-server-dynamic-secret-integration

fix: address sql server dynamic secret integration
This commit is contained in:
Sheen
2025-09-10 22:12:26 +08:00
committed by GitHub
4 changed files with 98 additions and 8 deletions

View File

@@ -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()
});

View File

@@ -1,4 +1,5 @@
import handlebars from "handlebars";
import RE2 from "re2";
import knex from "knex";
import { z } from "zod";
@@ -150,12 +151,30 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
return { ...providerInputs, hostIp };
};
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema> & { hostIp: string }) => {
const $getClient = async (
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema> & { hostIp: string; originalHost: string }
) => {
const ssl = providerInputs.ca
? { rejectUnauthorized: false, ca: providerInputs.ca, servername: providerInputs.host }
: undefined;
const isMsSQLClient = providerInputs.client === SqlProviders.MsSQL;
/*
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@<azure-server-name>" so Azure opens the correct logical server.
Direct connections to the Azure FQDN usually don’t require this suffix.
*/
const isAzureSql = isMsSQLClient && new RE2(/\.database\.windows\.net$/i).test(providerInputs.originalHost);
const azureServerLabel =
isAzureSql && providerInputs.gatewayId ? providerInputs.originalHost?.split(".")[0] : undefined;
const effectiveUser =
isAzureSql && !providerInputs.username.includes("@") && azureServerLabel
? `${providerInputs.username}@${azureServerLabel}`
: providerInputs.username;
const db = knex({
client: providerInputs.client,
connection: {
@@ -165,7 +184,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
providerInputs.client === SqlProviders.Postgres && !providerInputs.gatewayId
? 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
@@ -173,6 +192,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 } : {}
}
@@ -215,7 +235,13 @@ 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,
originalHost: providerInputs.host
});
// oracle needs from keyword
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
@@ -256,7 +282,12 @@ 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,
originalHost: providerInputs.host
});
try {
const expiration = new Date(expireAt).toISOString();
@@ -299,7 +330,12 @@ 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,
originalHost: providerInputs.host
});
try {
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
const queries = revokeStatement.toString().split(";").filter(Boolean);
@@ -334,7 +370,12 @@ 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,
originalHost: providerInputs.host
});
const expiration = new Date(expireAt).toISOString();
const { database } = providerInputs;

View File

@@ -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 = ({
/>
</div>
<div>
{selectedClient === SqlProviders.MsSQL && (
<div className="mb-2 mt-2">
<Controller
control={control}
name="provider.sslEnabled"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error?.message)} errorText={error?.message}>
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="sql-ds-ssl-enabled"
thumbClassName="bg-mineshaft-800"
isChecked={value}
onCheckedChange={onChange}
>
Encrypt Connection (SSL)
</Switch>
</FormControl>
)}
/>
</div>
)}
<Controller
control={control}
name="provider.ca"
render={({ field, fieldState: { error } }) => (
<FormControl
isOptional
label="CA(SSL)"
label="CA (SSL)"
isError={Boolean(error?.message)}
errorText={error?.message}
>

View File

@@ -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 = ({
/>
</div>
<div>
{selectedClient === SqlProviders.MsSQL && (
<div className="mb-2 mt-2">
<Controller
control={control}
name="inputs.sslEnabled"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error?.message)} errorText={error?.message}>
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="sql-ds-ssl-enabled"
thumbClassName="bg-mineshaft-800"
isChecked={Boolean(value)}
onCheckedChange={onChange}
>
Encrypt Connection (SSL)
</Switch>
</FormControl>
)}
/>
</div>
)}
<Controller
control={control}
name="inputs.ca"
render={({ field, fieldState: { error } }) => (
<FormControl
isOptional
label="CA(SSL)"
label="CA (SSL)"
isError={Boolean(error?.message)}
errorText={error?.message}
>