Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,629 changes: 1,532 additions & 1,097 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"simple-git": "^3.27.0",
"snowflake-sdk": "^2.0.3",
"snowflake-sdk": "^2.4.3",
"split-pane-react": "^0.1.3",
"tar": "^7.4.3",
"uuid": "^11.1.0",
Expand Down
19 changes: 16 additions & 3 deletions src/main/extractor/snowflake.extractor.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop, consistent-return */
import snowflake from 'snowflake-sdk';
import { Column, Table } from '../../types/backend';
import { Column, SnowflakeAuthMethod, Table } from '../../types/backend';

export default class SnowflakeExtractor {
private connection: snowflake.Connection;

constructor(config: {
account: string;
username: string;
password: string;
password?: string;
warehouse: string;
database: string;
schema: string;
role?: string;
authMethod?: SnowflakeAuthMethod;
}) {
this.connection = snowflake.createConnection(config);
const authMethod =
config.authMethod === 'web_browser' ? 'web_browser' : 'password';
this.connection = snowflake.createConnection({
account: config.account,
username: config.username,
...(authMethod === 'web_browser'
? { authenticator: 'EXTERNALBROWSER' as const }
: { password: config.password }),
warehouse: config.warehouse,
database: config.database,
schema: config.schema,
role: config.role,
});
}

async connect(): Promise<void> {
Expand Down
20 changes: 15 additions & 5 deletions src/main/services/ai/tools/studio/connections.tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import type {
MinIOConfig,
RustfsConfig,
} from '../../../../../types/frontend';
import type {
ConnectorTestResponse,
ConnectionTestResult,
BigQueryTestResponse,
} from '../../../../../types/backend';
import CloudExplorerService from '../../../cloudExplorer.service';
import ConnectorsService from '../../../connectors.service';
import DuckLakeService from '../../../duckLake.service';
Expand All @@ -20,14 +25,19 @@ const STUDIO_CLOUD_CONNECTION_TEST_FLAG = 'studio.cloud.connection_test';

type ConnectionHealth = 'healthy' | 'unhealthy' | 'unknown';

function toHealthLabel(
testResult: boolean | { success: boolean },
): ConnectionHealth {
function toHealthLabel(testResult: ConnectorTestResponse): ConnectionHealth {
if (typeof testResult === 'boolean') {
return testResult ? 'healthy' : 'unhealthy';
}
if (typeof testResult?.success === 'boolean') {
return testResult.success ? 'healthy' : 'unhealthy';
if (typeof testResult === 'object' && testResult !== null) {
if ('ok' in testResult) {
return (testResult as ConnectionTestResult).ok ? 'healthy' : 'unhealthy';
}
if ('success' in testResult) {
return (testResult as BigQueryTestResponse).success
? 'healthy'
: 'unhealthy';
}
}
return 'unknown';
}
Expand Down
24 changes: 18 additions & 6 deletions src/main/services/cloudExplorer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,9 @@ class CloudExplorerService {
Bucket: bucketName,
Key: objectKey,
});
return await getSignedUrl(client, command, { expiresIn: 3600 });
return await getSignedUrl(client as any, command as any, {
expiresIn: 3600,
});
} catch (error) {
throw new Error(`Error generating S3 signed URL: ${error}`);
}
Expand Down Expand Up @@ -653,7 +655,9 @@ class CloudExplorerService {
Bucket: bucketName,
Key: objectKey,
});
return await getSignedUrl(client, command, { expiresIn: 3600 });
return await getSignedUrl(client as any, command as any, {
expiresIn: 3600,
});
} catch (error) {
throw new Error(`Error generating MinIO signed URL: ${error}`);
}
Expand Down Expand Up @@ -835,7 +839,9 @@ class CloudExplorerService {
Bucket: bucketName,
Key: objectKey,
});
return await getSignedUrl(client, command, { expiresIn: 3600 });
return await getSignedUrl(client as any, command as any, {
expiresIn: 3600,
});
} catch (error) {
throw new Error(`Error generating Cloudflare R2 signed URL: ${error}`);
}
Expand Down Expand Up @@ -1033,7 +1039,9 @@ class CloudExplorerService {
Bucket: bucketName,
Key: objectKey,
});
return await getSignedUrl(client, command, { expiresIn: 3600 });
return await getSignedUrl(client as any, command as any, {
expiresIn: 3600,
});
} catch (error) {
throw new Error(`Error generating Backblaze B2 signed URL: ${error}`);
}
Expand Down Expand Up @@ -1217,7 +1225,9 @@ class CloudExplorerService {
Bucket: bucketName,
Key: objectKey,
});
return await getSignedUrl(client, command, { expiresIn: 3600 });
return await getSignedUrl(client as any, command as any, {
expiresIn: 3600,
});
} catch (error) {
throw new Error(`Error generating rustfs signed URL: ${error}`);
}
Expand Down Expand Up @@ -1403,7 +1413,9 @@ class CloudExplorerService {
Bucket: bucketName,
Key: objectKey,
});
return await getSignedUrl(client, command, { expiresIn: 3600 });
return await getSignedUrl(client as any, command as any, {
expiresIn: 3600,
});
} catch (error) {
throw new Error(`Error generating Garage signed URL: ${error}`);
}
Expand Down
111 changes: 99 additions & 12 deletions src/main/services/connectors.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { v4 as uuidV4 } from 'uuid';
import { NotebooksService } from './notebooks.service';
import {
BigQueryConnection,
BigQueryTestResponse,
ConnectionTestResult,
ConnectionInput,
ConnectorTestResponse,
ConnectionModel,
DatabricksConnection,
DBTConnection,
Expand All @@ -21,6 +22,7 @@ import {
QueryResponseType,
RedshiftConnection,
RosettaConnection,
SnowflakeAuthMethod,
SnowflakeConnection,
} from '../../types/backend';
import { loadDatabaseFile, updateDatabase } from '../utils/fileHelper';
Expand Down Expand Up @@ -50,6 +52,33 @@ import DuckLakeService from './duckLake.service';
import DuckLakeInstanceStore from './duckLake/instanceStore.service';

export default class ConnectorsService {
private static getSnowflakeAuthMethod(
connection: SnowflakeConnection,
): SnowflakeAuthMethod {
return connection.authMethod === 'web_browser' ? 'web_browser' : 'password';
}

private static logSnowflakeTestFailure(
connection: SnowflakeConnection,
result: ConnectionTestResult,
): void {
// eslint-disable-next-line no-console
console.error('[ConnectorsService] Snowflake test connection failed', {
scope: 'connectors',
provider: 'snowflake',
operation: 'testConnection',
authMethod: this.getSnowflakeAuthMethod(connection),
account: connection.account,
username: connection.username,
warehouse: connection.warehouse,
database: connection.database,
schema: connection.schema,
code: result.code,
message: result.message,
details: result.details,
});
Comment on lines +61 to +79

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not log Snowflake usernames or raw authentication details.

Usernames may be email addresses, while SDK/SSO details can contain sensitive identity-provider information. Log controlled codes/messages instead.

Proposed adjustment
       authMethod: this.getSnowflakeAuthMethod(connection),
       account: connection.account,
-      username: connection.username,
+      usernameProvided: Boolean(connection.username),
       warehouse: connection.warehouse,
       database: connection.database,
       schema: connection.schema,
       code: result.code,
       message: result.message,
-      details: result.details,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static logSnowflakeTestFailure(
connection: SnowflakeConnection,
result: ConnectionTestResult,
): void {
// eslint-disable-next-line no-console
console.error('[ConnectorsService] Snowflake test connection failed', {
scope: 'connectors',
provider: 'snowflake',
operation: 'testConnection',
authMethod: this.getSnowflakeAuthMethod(connection),
account: connection.account,
username: connection.username,
warehouse: connection.warehouse,
database: connection.database,
schema: connection.schema,
code: result.code,
message: result.message,
details: result.details,
});
private static logSnowflakeTestFailure(
connection: SnowflakeConnection,
result: ConnectionTestResult,
): void {
// eslint-disable-next-line no-console
console.error('[ConnectorsService] Snowflake test connection failed', {
scope: 'connectors',
provider: 'snowflake',
operation: 'testConnection',
authMethod: this.getSnowflakeAuthMethod(connection),
account: connection.account,
usernameProvided: Boolean(connection.username),
warehouse: connection.warehouse,
database: connection.database,
schema: connection.schema,
code: result.code,
message: result.message,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/services/connectors.service.ts` around lines 61 - 79, Update
ConnectorsService.logSnowflakeTestFailure to remove connection.username and any
raw authentication-related fields from the console.error payload. Retain only
controlled, non-sensitive identifiers and the structured result code/message;
avoid exposing SDK or SSO details while preserving the failure context.

}

static async loadConnections(
includeDataLake: boolean = false,
): Promise<ConnectionModel[]> {
Expand Down Expand Up @@ -135,7 +164,9 @@ export default class ConnectorsService {
(conn1 as SnowflakeConnection).warehouse ===
(conn2 as SnowflakeConnection).warehouse &&
(conn1 as SnowflakeConnection).schema ===
(conn2 as SnowflakeConnection).schema
(conn2 as SnowflakeConnection).schema &&
this.getSnowflakeAuthMethod(conn1 as SnowflakeConnection) ===
this.getSnowflakeAuthMethod(conn2 as SnowflakeConnection)
);

case 'bigquery':
Expand Down Expand Up @@ -647,17 +678,17 @@ export default class ConnectorsService {
*/
static async testConnection(
connection: ConnectionInput,
): Promise<boolean | BigQueryTestResponse> {
): Promise<ConnectorTestResponse> {
await this.validateConnection(connection);
switch (connection.type) {
case 'postgres':
return testPostgresConnection(connection);
case 'snowflake':
try {
return await testSnowflakeConnection(connection);
} catch {
return false;
const result = await testSnowflakeConnection(connection);
if (!result.ok) {
this.logSnowflakeTestFailure(connection, result);
}
return result;
Comment on lines 686 to +691

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Wrap the Snowflake switch clause in a block.

The lexical result declaration violates noSwitchDeclarations and can fail the lint pipeline.

Proposed fix
-      case 'snowflake':
+      case 'snowflake': {
         const result = await testSnowflakeConnection(connection);
         if (!result.ok) {
           this.logSnowflakeTestFailure(connection, result);
         }
         return result;
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case 'snowflake':
try {
return await testSnowflakeConnection(connection);
} catch {
return false;
const result = await testSnowflakeConnection(connection);
if (!result.ok) {
this.logSnowflakeTestFailure(connection, result);
}
return result;
case 'snowflake': {
const result = await testSnowflakeConnection(connection);
if (!result.ok) {
this.logSnowflakeTestFailure(connection, result);
}
return result;
}
🧰 Tools
🪛 Biome (2.5.3)

[error] 687-687: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/services/connectors.service.ts` around lines 686 - 691, Wrap the
`case 'snowflake'` clause in its own block so the `result` declaration is scoped
within that case and satisfies `noSwitchDeclarations`; preserve the existing
`testSnowflakeConnection`, failure logging, and return behavior.

Source: Linters/SAST tools

case 'bigquery':
return testBigQueryConnection(connection);
case 'databricks':
Expand Down Expand Up @@ -901,7 +932,14 @@ export default class ConnectorsService {
) {
// Only add userName/password for non-BigQuery, non-Databricks, non-DuckDB, non-ducklake
connectionConfig.userName = ev('user');
connectionConfig.password = ev('password');
if (
connection.type === 'snowflake' &&
this.getSnowflakeAuthMethod(connection) === 'web_browser'
) {
connectionConfig.authenticator = 'externalbrowser';
} else {
connectionConfig.password = ev('password');
}
}

const yamlData: {
Expand All @@ -928,6 +966,13 @@ export default class ConnectorsService {
case 'snowflake':
if (!conn.account) throw new Error('Snowflake account is required');
if (!('warehouse' in conn)) throw new Error('Warehouse is required');
if (!conn.username) throw new Error('Username is required');
if (
this.getSnowflakeAuthMethod(conn) === 'password' &&
!conn.password
) {
throw new Error('Password is required for Snowflake password auth');
}
break;
case 'bigquery':
if (!('project' in conn)) throw new Error('Project ID is required');
Expand Down Expand Up @@ -966,7 +1011,7 @@ export default class ConnectorsService {
return postgresUrl;
}
case 'snowflake':
return `jdbc:snowflake://${ev('account')}.snowflakecomputing.com/?warehouse=${ev('warehouse')}&db=${ev('dbname')}&schema=${ev('schema')}`;
return `jdbc:snowflake://${ev('account')}.snowflakecomputing.com/?warehouse=${ev('warehouse')}&db=${ev('dbname')}&schema=${ev('schema')}${this.getSnowflakeAuthMethod(conn) === 'web_browser' ? '&authenticator=externalbrowser' : ''}`;
case 'redshift': {
let redshiftUrl = `jdbc:redshift://${ev('host')}:${ev('port')}/${ev('dbname')}?currentSchema=${ev('schema')}`;

Expand Down Expand Up @@ -1059,16 +1104,35 @@ export default class ConnectorsService {
connection.type !== 'duckdb' &&
connection.type !== 'bigquery' &&
'username' in connection &&
'password' in connection && {
(connection.type !== 'snowflake' ||
this.getSnowflakeAuthMethod(connection) !== 'web_browser') && {
userName: `db-user-${connection.name}`,
password: `db-password-${connection.name}`,
}),
...(connection.type === 'snowflake' &&
this.getSnowflakeAuthMethod(connection) === 'web_browser' && {
userName: `db-user-${connection.name}`,
authenticator: 'externalbrowser',
}),
};
}

private static mapToDbtConnection(conn: ConnectionInput): DBTConnection {
switch (conn.type) {
case 'snowflake':
if (this.getSnowflakeAuthMethod(conn) === 'web_browser') {
return {
type: 'snowflake',
username: `db-user-${conn.name}`,
database: conn.database,
schema: conn.schema,
account: conn.accountLocator || conn.account,
warehouse: conn.warehouse,
...(conn.role && { role: conn.role }),
authMethod: 'web_browser',
authenticator: 'externalbrowser',
};
}
return {
type: 'snowflake',
username: `db-user-${conn.name}`,
Expand Down Expand Up @@ -1231,6 +1295,19 @@ export default class ConnectorsService {
...(conn.ssl && { sslmode: 'require' }),
};
case 'snowflake':
if (this.getSnowflakeAuthMethod(conn) === 'web_browser') {
return {
type: 'snowflake',
account: envVar('account'),
user: envVar('user'),
authenticator: 'externalbrowser',
...(conn.role && { role: envVar('role') }),
warehouse: envVar('warehouse'),
database: envVar('dbname'),
schema: envVar('schema'),
threads: 4,
};
}
return {
type: 'snowflake',
account: envVar('account'),
Expand Down Expand Up @@ -1443,11 +1520,15 @@ export default class ConnectorsService {
type: 'snowflake',
account: devOutput.account,
username: devOutput.user,
password: devOutput.password,
password: devOutput.password || '',
database: devOutput.database,
schema: devOutput.schema,
warehouse: devOutput.warehouse,
role: devOutput.role,
authMethod:
devOutput.authenticator === 'externalbrowser'
? 'web_browser'
: 'password',
};

case 'bigquery':
Expand Down Expand Up @@ -1542,11 +1623,16 @@ export default class ConnectorsService {
account: dbtConnection.account,
warehouse: dbtConnection.warehouse,
username: dbtConnection.username,
password: dbtConnection.password,
password: dbtConnection.password || '',
database: dbtConnection.database,
schema: dbtConnection.schema,
role: dbtConnection.role,
client_session_keep_alive: dbtConnection.client_session_keep_alive,
authMethod:
dbtConnection.authenticator === 'externalbrowser' ||
dbtConnection.authMethod === 'web_browser'
? 'web_browser'
: 'password',
};

case 'bigquery':
Expand Down Expand Up @@ -1909,6 +1995,7 @@ export default class ConnectorsService {
database: sfConn.database,
schema: sfConn.schema,
role: sfConn.role,
authMethod: this.getSnowflakeAuthMethod(sfConn),
});
try {
await extractor.connect();
Expand Down
1 change: 1 addition & 0 deletions src/main/services/projects.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,7 @@ export default class ProjectsService {
database: connection.database,
schema: connection.schema,
role: connection.role,
authMethod: connection.authMethod,
});

await extractor.connect();
Expand Down
Loading
Loading