fix: harden security, reduce duplication, and improve robustness

- Fix SQL injection in data.rs by wrapping get_table_data in READ ONLY transaction
- Fix SQL injection in docker.rs CREATE DATABASE via escape_ident
- Fix command injection in docker.rs by validating pg_version/container_name
  and escaping shell-interpolated values
- Fix UTF-8 panic on stderr truncation with char_indices
- Wrap delete_rows in a transaction for atomicity
- Replace .expect() with proper error propagation in lib.rs
- Cache AI settings in AppState to avoid repeated disk reads
- Cap JSONB column discovery at 50 to prevent unbounded queries
- Fix ERD colorMode to respect system theme via useTheme()
- Extract AppState::get_pool() replacing ~19 inline pool patterns
- Extract shared AiSettingsFields component (DRY popover + sheet)
- Make get_connections_path pub(crate) and reuse from docker.rs
- Deduplicate check_docker by delegating to check_docker_internal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 11:41:14 +03:00
parent baa794b66a
commit d507162377
15 changed files with 1196 additions and 667 deletions

View File

@@ -14,17 +14,14 @@ pub async fn list_databases(
state: State<'_, Arc<AppState>>,
connection_id: String,
) -> TuskResult<Vec<String>> {
let pools = state.pools.read().await;
let pool = pools
.get(&connection_id)
.ok_or(TuskError::NotConnected(connection_id))?;
let pool = state.get_pool(&connection_id).await?;
let rows = sqlx::query(
"SELECT datname FROM pg_database \
WHERE datistemplate = false \
ORDER BY datname",
)
.fetch_all(pool)
.fetch_all(&pool)
.await
.map_err(TuskError::Database)?;
@@ -35,10 +32,7 @@ pub async fn list_schemas_core(
state: &AppState,
connection_id: &str,
) -> TuskResult<Vec<String>> {
let pools = state.pools.read().await;
let pool = pools
.get(connection_id)
.ok_or_else(|| TuskError::NotConnected(connection_id.to_string()))?;
let pool = state.get_pool(connection_id).await?;
let flavor = state.get_flavor(connection_id).await;
let sql = if flavor == DbFlavor::Greenplum {
@@ -52,7 +46,7 @@ pub async fn list_schemas_core(
};
let rows = sqlx::query(sql)
.fetch_all(pool)
.fetch_all(&pool)
.await
.map_err(TuskError::Database)?;
@@ -72,10 +66,7 @@ pub async fn list_tables_core(
connection_id: &str,
schema: &str,
) -> TuskResult<Vec<SchemaObject>> {
let pools = state.pools.read().await;
let pool = pools
.get(connection_id)
.ok_or_else(|| TuskError::NotConnected(connection_id.to_string()))?;
let pool = state.get_pool(connection_id).await?;
let rows = sqlx::query(
"SELECT t.table_name, \
@@ -88,7 +79,7 @@ pub async fn list_tables_core(
ORDER BY t.table_name",
)
.bind(schema)
.fetch_all(pool)
.fetch_all(&pool)
.await
.map_err(TuskError::Database)?;
@@ -119,10 +110,7 @@ pub async fn list_views(
connection_id: String,
schema: String,
) -> TuskResult<Vec<SchemaObject>> {
let pools = state.pools.read().await;
let pool = pools
.get(&connection_id)
.ok_or(TuskError::NotConnected(connection_id))?;
let pool = state.get_pool(&connection_id).await?;
let rows = sqlx::query(
"SELECT table_name FROM information_schema.views \
@@ -130,7 +118,7 @@ pub async fn list_views(
ORDER BY table_name",
)
.bind(&schema)
.fetch_all(pool)
.fetch_all(&pool)
.await
.map_err(TuskError::Database)?;
@@ -152,10 +140,7 @@ pub async fn list_functions(
connection_id: String,
schema: String,
) -> TuskResult<Vec<SchemaObject>> {
let pools = state.pools.read().await;
let pool = pools
.get(&connection_id)
.ok_or(TuskError::NotConnected(connection_id))?;
let pool = state.get_pool(&connection_id).await?;
let rows = sqlx::query(
"SELECT routine_name FROM information_schema.routines \
@@ -163,7 +148,7 @@ pub async fn list_functions(
ORDER BY routine_name",
)
.bind(&schema)
.fetch_all(pool)
.fetch_all(&pool)
.await
.map_err(TuskError::Database)?;
@@ -185,10 +170,7 @@ pub async fn list_indexes(
connection_id: String,
schema: String,
) -> TuskResult<Vec<SchemaObject>> {
let pools = state.pools.read().await;
let pool = pools
.get(&connection_id)
.ok_or(TuskError::NotConnected(connection_id))?;
let pool = state.get_pool(&connection_id).await?;
let rows = sqlx::query(
"SELECT indexname FROM pg_indexes \
@@ -196,7 +178,7 @@ pub async fn list_indexes(
ORDER BY indexname",
)
.bind(&schema)
.fetch_all(pool)
.fetch_all(&pool)
.await
.map_err(TuskError::Database)?;
@@ -218,10 +200,7 @@ pub async fn list_sequences(
connection_id: String,
schema: String,
) -> TuskResult<Vec<SchemaObject>> {
let pools = state.pools.read().await;
let pool = pools
.get(&connection_id)
.ok_or(TuskError::NotConnected(connection_id))?;
let pool = state.get_pool(&connection_id).await?;
let rows = sqlx::query(
"SELECT sequence_name FROM information_schema.sequences \
@@ -229,7 +208,7 @@ pub async fn list_sequences(
ORDER BY sequence_name",
)
.bind(&schema)
.fetch_all(pool)
.fetch_all(&pool)
.await
.map_err(TuskError::Database)?;
@@ -251,10 +230,7 @@ pub async fn get_table_columns_core(
schema: &str,
table: &str,
) -> TuskResult<Vec<ColumnInfo>> {
let pools = state.pools.read().await;
let pool = pools
.get(connection_id)
.ok_or_else(|| TuskError::NotConnected(connection_id.to_string()))?;
let pool = state.get_pool(connection_id).await?;
let rows = sqlx::query(
"SELECT \
@@ -287,7 +263,7 @@ pub async fn get_table_columns_core(
)
.bind(schema)
.bind(table)
.fetch_all(pool)
.fetch_all(&pool)
.await
.map_err(TuskError::Database)?;
@@ -323,10 +299,7 @@ pub async fn get_table_constraints(
schema: String,
table: String,
) -> TuskResult<Vec<ConstraintInfo>> {
let pools = state.pools.read().await;
let pool = pools
.get(&connection_id)
.ok_or(TuskError::NotConnected(connection_id))?;
let pool = state.get_pool(&connection_id).await?;
let rows = sqlx::query(
"SELECT \
@@ -376,7 +349,7 @@ pub async fn get_table_constraints(
)
.bind(&schema)
.bind(&table)
.fetch_all(pool)
.fetch_all(&pool)
.await
.map_err(TuskError::Database)?;
@@ -402,10 +375,7 @@ pub async fn get_table_indexes(
schema: String,
table: String,
) -> TuskResult<Vec<IndexInfo>> {
let pools = state.pools.read().await;
let pool = pools
.get(&connection_id)
.ok_or(TuskError::NotConnected(connection_id))?;
let pool = state.get_pool(&connection_id).await?;
let rows = sqlx::query(
"SELECT \
@@ -422,7 +392,7 @@ pub async fn get_table_indexes(
)
.bind(&schema)
.bind(&table)
.fetch_all(pool)
.fetch_all(&pool)
.await
.map_err(TuskError::Database)?;
@@ -443,10 +413,7 @@ pub async fn get_completion_schema(
connection_id: String,
) -> TuskResult<HashMap<String, HashMap<String, Vec<String>>>> {
let flavor = state.get_flavor(&connection_id).await;
let pools = state.pools.read().await;
let pool = pools
.get(&connection_id)
.ok_or(TuskError::NotConnected(connection_id))?;
let pool = state.get_pool(&connection_id).await?;
let sql = if flavor == DbFlavor::Greenplum {
"SELECT table_schema, table_name, column_name \
@@ -461,7 +428,7 @@ pub async fn get_completion_schema(
};
let rows = sqlx::query(sql)
.fetch_all(pool)
.fetch_all(&pool)
.await
.map_err(TuskError::Database)?;
@@ -490,10 +457,7 @@ pub async fn get_column_details(
table: String,
) -> TuskResult<Vec<ColumnDetail>> {
let flavor = state.get_flavor(&connection_id).await;
let pools = state.pools.read().await;
let pool = pools
.get(&connection_id)
.ok_or(TuskError::NotConnected(connection_id))?;
let pool = state.get_pool(&connection_id).await?;
let sql = if flavor == DbFlavor::Greenplum {
"SELECT c.column_name, c.data_type, \
@@ -516,7 +480,7 @@ pub async fn get_column_details(
let rows = sqlx::query(sql)
.bind(&schema)
.bind(&table)
.fetch_all(pool)
.fetch_all(&pool)
.await
.map_err(TuskError::Database)?;
@@ -539,10 +503,7 @@ pub async fn get_table_triggers(
schema: String,
table: String,
) -> TuskResult<Vec<TriggerInfo>> {
let pools = state.pools.read().await;
let pool = pools
.get(&connection_id)
.ok_or(TuskError::NotConnected(connection_id))?;
let pool = state.get_pool(&connection_id).await?;
let rows = sqlx::query(
"SELECT \
@@ -571,7 +532,7 @@ pub async fn get_table_triggers(
)
.bind(&schema)
.bind(&table)
.fetch_all(pool)
.fetch_all(&pool)
.await
.map_err(TuskError::Database)?;
@@ -595,10 +556,7 @@ pub async fn get_schema_erd(
connection_id: String,
schema: String,
) -> TuskResult<ErdData> {
let pools = state.pools.read().await;
let pool = pools
.get(&connection_id)
.ok_or(TuskError::NotConnected(connection_id))?;
let pool = state.get_pool(&connection_id).await?;
// Get all tables with columns
let col_rows = sqlx::query(
@@ -627,7 +585,7 @@ pub async fn get_schema_erd(
ORDER BY c.table_name, c.ordinal_position",
)
.bind(&schema)
.fetch_all(pool)
.fetch_all(&pool)
.await
.map_err(TuskError::Database)?;
@@ -690,7 +648,7 @@ pub async fn get_schema_erd(
ORDER BY c.conname",
)
.bind(&schema)
.fetch_all(pool)
.fetch_all(&pool)
.await
.map_err(TuskError::Database)?;