From bf72e2fb24c05f8ad009c0052228f50680b98ffc Mon Sep 17 00:00:00 2001 From: James Sadler Date: Wed, 29 Jul 2026 23:20:54 +1000 Subject: [PATCH 1/4] test(integration): take the suite's ports from the environment The four ports the integration suite connects to were hardcoded, so only one copy of the suite could run at a time. Several agents each working on a different Proxy defect need their own Proxy and PostgreSQL, which means their own ports. Each port now reads an environment variable and falls back to the current value, so an unconfigured run behaves exactly as before. A malformed value panics rather than falling back: silently retargeting the whole suite at whatever else is listening on 6432 is the one failure that looks like a pass. --- .../src/common.rs | 61 +++++++++++++------ .../src/connection_resilience.rs | 16 ++--- .../src/decrypt/insert_returning.rs | 6 +- .../src/diagnostics.rs | 6 +- .../src/disable_mapping.rs | 8 +-- .../src/empty_result.rs | 2 +- .../src/encryption_sanity.rs | 16 ++--- .../src/eql_regression.rs | 8 +-- .../src/extended_protocol_error_messages.rs | 8 +-- .../src/insert/insert_domain_type.rs | 2 +- .../src/insert/insert_with_params.rs | 2 +- .../src/map_concat.rs | 2 +- .../src/map_literals.rs | 12 ++-- .../src/map_match_index.rs | 2 +- .../src/map_nulls.rs | 10 +-- .../src/map_ope_index_order.rs | 6 +- .../src/map_ope_index_where.rs | 2 +- .../src/map_ore_index_order.rs | 36 +++++------ .../src/map_ore_index_where.rs | 2 +- .../src/map_params.rs | 2 +- .../src/map_unique_index.rs | 16 ++--- .../src/migrate/mod.rs | 2 +- .../src/multitenant/contention.rs | 2 +- .../src/multitenant/ore_order.rs | 2 +- .../src/multitenant/set_keyset_id.rs | 14 ++--- .../src/multitenant/set_keyset_name.rs | 16 ++--- .../src/passthrough.rs | 16 ++--- .../src/pipeline.rs | 2 +- .../src/schema_change.rs | 2 +- .../src/select/distinct_order_by.rs | 14 ++--- .../src/select/indexing.rs | 2 +- .../src/select/jsonb_array_elements.rs | 6 +- .../src/select/jsonb_containment_index.rs | 6 +- .../src/select/jsonb_fusion_gaps.rs | 4 +- .../src/select/jsonb_path_query.rs | 10 +-- .../src/select/jsonb_selector_param_types.rs | 6 +- .../src/select/operator_backed_predicates.rs | 4 +- .../src/select/operator_class_shapes.rs | 10 +-- .../src/select/pg_catalog.rs | 2 +- .../src/select/select_where_in.rs | 8 +-- .../src/select/select_where_jsonb.rs | 2 +- .../src/select/unmappable.rs | 8 +-- .../src/set_keyset_error.rs | 4 +- .../src/simple_protocol/error_handling.rs | 2 +- .../src/simple_protocol/map_literals.rs | 14 ++--- .../src/simple_protocol/map_nulls.rs | 4 +- .../simple_protocol/multiple_statements.rs | 4 +- .../src/update/update_domain_type.rs | 2 +- .../src/update/update_with_reused_param.rs | 4 +- 49 files changed, 209 insertions(+), 188 deletions(-) diff --git a/packages/cipherstash-proxy-integration/src/common.rs b/packages/cipherstash-proxy-integration/src/common.rs index 4e4157b1f..83c83d414 100644 --- a/packages/cipherstash-proxy-integration/src/common.rs +++ b/packages/cipherstash-proxy-integration/src/common.rs @@ -14,7 +14,7 @@ //! ```rust //! #[tokio::test] //! async fn my_test() { -//! let client = connect_with_tls(PROXY).await; +//! let client = connect_with_tls(*PROXY).await; //! clear_with_client(&client).await; //! insert_with_client(sql, params, &client).await; //! query_by_with_client(sql, param, &client).await; @@ -41,15 +41,36 @@ use rustls::{ pki_types::CertificateDer, ClientConfig, }; use serde_json::Value; -use std::sync::{Arc, Once}; +use std::sync::{Arc, LazyLock, Once}; use tokio_postgres::{types::ToSql, Client, NoTls, Row, SimpleQueryMessage}; use tracing::info; use tracing_subscriber::{filter::Directive, EnvFilter, FmtSubscriber}; -pub const PROXY: u16 = 6432; -pub const PROXY_METRICS_PORT: u16 = 9930; -pub const PG_PORT: u16 = 5532; -pub const PG_TLS_PORT: u16 = 5617; +/// The ports the suite connects to, defaulting to the standard dev ports. +/// +/// Each is overridable by environment variable so that several copies of this +/// suite can run at once, each against its own Proxy and PostgreSQL. Set these +/// to match the `CS_SERVER__PORT` and `CS_DATABASE__PORT` the Proxy under test +/// was started with — nothing here starts anything, it only decides where to +/// connect. +pub static PROXY: LazyLock = LazyLock::new(|| port_from_env("CS_TEST_PROXY_PORT", 6432)); +pub static PROXY_METRICS_PORT: LazyLock = + LazyLock::new(|| port_from_env("CS_TEST_PROXY_METRICS_PORT", 9930)); +pub static PG_PORT: LazyLock = LazyLock::new(|| port_from_env("CS_TEST_PG_PORT", 5532)); +pub static PG_TLS_PORT: LazyLock = + LazyLock::new(|| port_from_env("CS_TEST_PG_TLS_PORT", 5617)); + +/// Panics rather than falling back to the default: a typo'd port would +/// otherwise send the whole suite at whatever is already listening on 6432, +/// which is the one outcome that looks like a pass and isn't. +fn port_from_env(var: &str, default: u16) -> u16 { + match std::env::var(var) { + Ok(value) => value + .parse() + .unwrap_or_else(|_| panic!("{var} must be a port number, got: {value:?}")), + Err(_) => default, + } +} pub const TEST_SCHEMA_SQL: &str = include_str!(concat!("../../../tests/sql/schema.sql")); @@ -77,7 +98,7 @@ pub fn random_string() -> String { } pub async fn clear() { - clear_with_client(&connect_with_tls(PROXY).await).await; + clear_with_client(&connect_with_tls(*PROXY).await).await; } pub async fn clear_with_client(client: &Client) { @@ -97,13 +118,13 @@ pub async fn clear_table_with_client(client: &Client, table: &str) { } pub async fn clear_table(table: &str) { - clear_table_with_client(&connect_with_tls(PROXY).await, table).await; + clear_table_with_client(&connect_with_tls(*PROXY).await, table).await; } pub async fn reset_schema() { let port = std::env::var("CS_DATABASE__PORT") .map(|s| s.parse().unwrap()) - .unwrap_or(PG_PORT); + .unwrap_or(*PG_PORT); let client = connect_with_tls(port).await; client.simple_query(TEST_SCHEMA_SQL).await.unwrap(); @@ -112,7 +133,7 @@ pub async fn reset_schema() { pub async fn reset_schema_to(schema: &'static str) { let port = std::env::var("CS_DATABASE__PORT") .map(|s| s.parse().unwrap()) - .unwrap_or(PG_PORT); + .unwrap_or(*PG_PORT); let client = connect_with_tls(port).await; client.simple_query(schema).await.unwrap(); @@ -132,7 +153,7 @@ pub async fn table_exists(table: &str) -> bool { let port = std::env::var("CS_DATABASE__PORT") .map(|s| s.parse().unwrap()) - .unwrap_or(PG_PORT); + .unwrap_or(*PG_PORT); let client = connect_with_tls(port).await; let messages = client.simple_query(&query).await.unwrap(); @@ -215,19 +236,19 @@ pub async fn connect(port: u16) -> Client { } pub async fn execute_query(sql: &str, params: &[&(dyn ToSql + Sync)]) { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; client.query(sql, params).await.unwrap(); } pub async fn execute_simple_query(sql: &str) { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; client.simple_query(sql).await.unwrap(); } pub async fn query tokio_postgres::types::FromSql<'a> + Send + Sync>( sql: &str, ) -> Vec { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; query_with_client(sql, &client).await } @@ -267,7 +288,7 @@ pub async fn query_by_params(sql: &str, params: &[&(dyn ToSql + Sync)]) -> Ve where T: for<'a> tokio_postgres::types::FromSql<'a> + Send + Sync, { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; query_by_params_with_client(sql, params, &client).await } @@ -288,7 +309,7 @@ pub fn get_database_port() -> u16 { std::env::var("CS_DATABASE__PORT") .ok() .and_then(|s| s.parse().ok()) - .unwrap_or(PG_PORT) + .unwrap_or(*PG_PORT) } pub async fn query_direct_by(sql: &str, param: &(dyn ToSql + Sync)) -> Vec @@ -307,7 +328,7 @@ pub async fn simple_query(sql: &str) -> Vec where ::Err: std::fmt::Debug, { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; simple_query_with_client(sql, &client).await } @@ -341,7 +362,7 @@ where // Returns a vector of `Option` for each row in the result set. // Nulls are represented as `None`, and non-null values are converted to `Some(String)`. pub async fn simple_query_with_null(sql: &str) -> Vec> { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let rows = client.simple_query(sql).await.unwrap(); rows.iter() .filter_map(|row| { @@ -355,7 +376,7 @@ pub async fn simple_query_with_null(sql: &str) -> Vec> { } pub async fn insert(sql: &str, params: &[&(dyn ToSql + Sync)]) { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; insert_with_client(sql, params, &client).await; } @@ -364,7 +385,7 @@ pub async fn insert_with_client(sql: &str, params: &[&(dyn ToSql + Sync)], clien } pub async fn insert_jsonb() -> Value { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; insert_jsonb_with_client(&client).await } diff --git a/packages/cipherstash-proxy-integration/src/connection_resilience.rs b/packages/cipherstash-proxy-integration/src/connection_resilience.rs index 0e8e9c182..fe1f67726 100644 --- a/packages/cipherstash-proxy-integration/src/connection_resilience.rs +++ b/packages/cipherstash-proxy-integration/src/connection_resilience.rs @@ -21,8 +21,8 @@ mod tests { #[tokio::test] async fn slow_query_does_not_block_other_connections() { let result = timeout(Duration::from_secs(30), async { - let client_a = connect_with_tls(PROXY).await; - let client_b = connect_with_tls(PROXY).await; + let client_a = connect_with_tls(*PROXY).await; + let client_b = connect_with_tls(*PROXY).await; // Connection A: run a slow query let a_handle = tokio::spawn(async move { @@ -56,7 +56,7 @@ mod tests { let result = timeout(Duration::from_secs(10), async { // First connection: query, then drop { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let rows = client.simple_query("SELECT 1").await.unwrap(); assert!(!rows.is_empty()); } @@ -66,7 +66,7 @@ mod tests { tokio::time::sleep(Duration::from_millis(100)).await; // Second connection: should work fine - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let rows = client.simple_query("SELECT 1").await.unwrap(); assert!(!rows.is_empty()); }) @@ -84,7 +84,7 @@ mod tests { // 5 slow connections for _ in 0..5 { join_set.spawn(async { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; client.simple_query("SELECT pg_sleep(3)").await.unwrap(); }); } @@ -96,7 +96,7 @@ mod tests { for _ in 0..5 { join_set.spawn(async { let start = Instant::now(); - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let rows = client.simple_query("SELECT 1").await.unwrap(); let elapsed = start.elapsed(); @@ -140,7 +140,7 @@ mod tests { // Connection B: through proxy, attempt to acquire the same lock (will block) let b_handle = tokio::spawn(async move { - let client_b = connect_with_tls(PROXY).await; + let client_b = connect_with_tls(*PROXY).await; // This will block until A releases the lock client_b .simple_query(&b_lock_query) @@ -173,7 +173,7 @@ mod tests { // Connection C: through proxy, should complete immediately despite B being blocked let start = Instant::now(); - let client_c = connect_with_tls(PROXY).await; + let client_c = connect_with_tls(*PROXY).await; let rows = client_c.simple_query("SELECT 1").await.unwrap(); let elapsed = start.elapsed(); diff --git a/packages/cipherstash-proxy-integration/src/decrypt/insert_returning.rs b/packages/cipherstash-proxy-integration/src/decrypt/insert_returning.rs index 074a96016..c2b4f0272 100644 --- a/packages/cipherstash-proxy-integration/src/decrypt/insert_returning.rs +++ b/packages/cipherstash-proxy-integration/src/decrypt/insert_returning.rs @@ -10,7 +10,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let plaintext = "plaintext"; @@ -59,7 +59,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let plaintext = "plaintext"; @@ -101,7 +101,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let plaintext = "plaintext"; diff --git a/packages/cipherstash-proxy-integration/src/diagnostics.rs b/packages/cipherstash-proxy-integration/src/diagnostics.rs index 72fcedf70..25edc4926 100644 --- a/packages/cipherstash-proxy-integration/src/diagnostics.rs +++ b/packages/cipherstash-proxy-integration/src/diagnostics.rs @@ -14,7 +14,7 @@ mod tests { /// Fetch metrics with retry logic to handle CI timing variability. async fn fetch_metrics_with_retry(max_retries: u32, delay_ms: u64) -> String { - let url = format!("http://localhost:{}/metrics", PROXY_METRICS_PORT); + let url = format!("http://localhost:{}/metrics", *PROXY_METRICS_PORT); let mut last_error = None; for attempt in 0..max_retries { @@ -40,7 +40,7 @@ mod tests { #[tokio::test] async fn metrics_include_statement_labels() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; clear().await; @@ -80,7 +80,7 @@ mod tests { #[tokio::test] async fn slow_statement_metrics_and_logs() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; clear().await; diff --git a/packages/cipherstash-proxy-integration/src/disable_mapping.rs b/packages/cipherstash-proxy-integration/src/disable_mapping.rs index 259b4b4cb..6ba7d9739 100644 --- a/packages/cipherstash-proxy-integration/src/disable_mapping.rs +++ b/packages/cipherstash-proxy-integration/src/disable_mapping.rs @@ -24,7 +24,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let encrypted_text = "hello".to_string(); @@ -74,7 +74,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); @@ -143,7 +143,7 @@ mod tests { let sql = "INSERT INTO encrypted (id, encrypted_text) VALUES ($1, $2)"; execute_query(sql, &[&id, &encrypted_text]).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SET CIPHERSTASH.UNSAFE_DISABLE_MAPPING = true"; client.query(sql, &[]).await.unwrap(); @@ -151,7 +151,7 @@ mod tests { // Mapping is NOT disabled for these queries for _ in 1..5 { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let actual = query_with_client::(select_sql, &client).await; diff --git a/packages/cipherstash-proxy-integration/src/empty_result.rs b/packages/cipherstash-proxy-integration/src/empty_result.rs index dd925cb85..811851878 100644 --- a/packages/cipherstash-proxy-integration/src/empty_result.rs +++ b/packages/cipherstash-proxy-integration/src/empty_result.rs @@ -4,7 +4,7 @@ mod tests { #[tokio::test] async fn empty_result_regression() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SELECT ''"; diff --git a/packages/cipherstash-proxy-integration/src/encryption_sanity.rs b/packages/cipherstash-proxy-integration/src/encryption_sanity.rs index 2e5e8ee47..71f362db4 100644 --- a/packages/cipherstash-proxy-integration/src/encryption_sanity.rs +++ b/packages/cipherstash-proxy-integration/src/encryption_sanity.rs @@ -22,7 +22,7 @@ mod tests { let plaintext = "hello world"; // Insert through proxy (should encrypt) - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "INSERT INTO encrypted (id, encrypted_text) VALUES ($1, $2)"; client.query(sql, &[&id, &plaintext]).await.unwrap(); @@ -49,7 +49,7 @@ mod tests { let plaintext_json = serde_json::json!({"key": "value", "number": 42}); // Insert through proxy (should encrypt) - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "INSERT INTO encrypted (id, encrypted_jsonb) VALUES ($1, $2)"; client.query(sql, &[&id, &plaintext_json]).await.unwrap(); @@ -76,7 +76,7 @@ mod tests { let plaintext: f64 = 123.456; // Insert through proxy (should encrypt) - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "INSERT INTO encrypted (id, encrypted_float8) VALUES ($1, $2)"; client.query(sql, &[&id, &plaintext]).await.unwrap(); @@ -103,7 +103,7 @@ mod tests { let plaintext: bool = true; // Insert through proxy (should encrypt) - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "INSERT INTO encrypted (id, encrypted_bool) VALUES ($1, $2)"; client.query(sql, &[&id, &plaintext]).await.unwrap(); @@ -130,7 +130,7 @@ mod tests { let plaintext = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(); // Insert through proxy (should encrypt) - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "INSERT INTO encrypted (id, encrypted_date) VALUES ($1, $2)"; client.query(sql, &[&id, &plaintext]).await.unwrap(); @@ -157,7 +157,7 @@ mod tests { let plaintext: i16 = 42; // Insert through proxy (should encrypt) - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "INSERT INTO encrypted (id, encrypted_int2) VALUES ($1, $2)"; client.query(sql, &[&id, &plaintext]).await.unwrap(); @@ -184,7 +184,7 @@ mod tests { let plaintext: i32 = 12345; // Insert through proxy (should encrypt) - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "INSERT INTO encrypted (id, encrypted_int4) VALUES ($1, $2)"; client.query(sql, &[&id, &plaintext]).await.unwrap(); @@ -211,7 +211,7 @@ mod tests { let plaintext: i64 = 9876543210; // Insert through proxy (should encrypt) - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "INSERT INTO encrypted (id, encrypted_int8) VALUES ($1, $2)"; client.query(sql, &[&id, &plaintext]).await.unwrap(); diff --git a/packages/cipherstash-proxy-integration/src/eql_regression.rs b/packages/cipherstash-proxy-integration/src/eql_regression.rs index aa1a08900..e8e3580de 100644 --- a/packages/cipherstash-proxy-integration/src/eql_regression.rs +++ b/packages/cipherstash-proxy-integration/src/eql_regression.rs @@ -73,7 +73,7 @@ mod tests { let id = random_id(); // Insert via proxy (will encrypt) - let proxy_client = connect_with_tls(PROXY).await; + let proxy_client = connect_with_tls(*PROXY).await; let sql = format!("INSERT INTO encrypted (id, {column}) VALUES ($1, $2)"); proxy_client .execute(&sql, &[&id, plaintext]) @@ -117,7 +117,7 @@ mod tests { where T: for<'a> tokio_postgres::types::FromSql<'a>, { - let proxy_client = connect_with_tls(PROXY).await; + let proxy_client = connect_with_tls(*PROXY).await; let sql = format!("SELECT {column} FROM encrypted WHERE id = $1"); let rows = proxy_client .query(&sql, &[&id]) @@ -496,7 +496,7 @@ mod tests { insert_encrypted_directly(id, "encrypted_jsonb", &fixture.ciphertext).await; // Test field access via proxy - let proxy_client = connect_with_tls(PROXY).await; + let proxy_client = connect_with_tls(*PROXY).await; // Access string field let sql = "SELECT encrypted_jsonb->'string' FROM encrypted WHERE id = $1"; @@ -547,7 +547,7 @@ mod tests { let id = random_id(); insert_encrypted_directly(id, "encrypted_jsonb", &fixture.ciphertext).await; - let proxy_client = connect_with_tls(PROXY).await; + let proxy_client = connect_with_tls(*PROXY).await; // Access array field let sql = "SELECT encrypted_jsonb->'array_number' FROM encrypted WHERE id = $1"; diff --git a/packages/cipherstash-proxy-integration/src/extended_protocol_error_messages.rs b/packages/cipherstash-proxy-integration/src/extended_protocol_error_messages.rs index 65f07ac3e..dbeccae4d 100644 --- a/packages/cipherstash-proxy-integration/src/extended_protocol_error_messages.rs +++ b/packages/cipherstash-proxy-integration/src/extended_protocol_error_messages.rs @@ -26,7 +26,7 @@ mod tests { let id = random_id(); - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let encrypted_text = "hello@cipherstash.com"; @@ -62,7 +62,7 @@ mod tests { reset_schema().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let _reset = Reset; @@ -89,7 +89,7 @@ mod tests { async fn mapper_unsupported_parameter_type_with_date() { trace(); - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); // let encrypted_date = NaiveDate::parse_from_str("2025-01-01", "%Y-%m-%d").unwrap(); @@ -114,7 +114,7 @@ mod tests { reset_schema().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let _reset = Reset; diff --git a/packages/cipherstash-proxy-integration/src/insert/insert_domain_type.rs b/packages/cipherstash-proxy-integration/src/insert/insert_domain_type.rs index 9277cb9a9..8ed9a4cac 100644 --- a/packages/cipherstash-proxy-integration/src/insert/insert_domain_type.rs +++ b/packages/cipherstash-proxy-integration/src/insert/insert_domain_type.rs @@ -40,7 +40,7 @@ mod tests { let sql = "INSERT INTO encrypted (id, plaintext_domain, encrypted_text) VALUES ($1, $2, $3) RETURNING id, plaintext_domain, encrypted_text"; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let result = client .query(sql, &[&id, &encrypted_domain, &encrypted_text]) .await diff --git a/packages/cipherstash-proxy-integration/src/insert/insert_with_params.rs b/packages/cipherstash-proxy-integration/src/insert/insert_with_params.rs index 170e0e688..a84452614 100644 --- a/packages/cipherstash-proxy-integration/src/insert/insert_with_params.rs +++ b/packages/cipherstash-proxy-integration/src/insert/insert_with_params.rs @@ -71,7 +71,7 @@ mod tests { pub async fn query tokio_postgres::types::FromSql<'a> + Send + Sync>( sql: &str, ) -> Vec { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let rows = client.query(sql, &[]).await.unwrap(); rows.iter().map(|row| row.get(0)).collect::>() } diff --git a/packages/cipherstash-proxy-integration/src/map_concat.rs b/packages/cipherstash-proxy-integration/src/map_concat.rs index 21e529c8f..b6fe2d818 100644 --- a/packages/cipherstash-proxy-integration/src/map_concat.rs +++ b/packages/cipherstash-proxy-integration/src/map_concat.rs @@ -4,7 +4,7 @@ mod tests { #[tokio::test] async fn map_concat_regression() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; clear().await; diff --git a/packages/cipherstash-proxy-integration/src/map_literals.rs b/packages/cipherstash-proxy-integration/src/map_literals.rs index 0d60ad5f1..ea132d7ef 100644 --- a/packages/cipherstash-proxy-integration/src/map_literals.rs +++ b/packages/cipherstash-proxy-integration/src/map_literals.rs @@ -6,7 +6,7 @@ mod tests { async fn map_literal() { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let encrypted_text = "hello@cipherstash.com"; @@ -26,7 +26,7 @@ mod tests { async fn map_literal_with_param() { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let encrypted_text = "hello@cipherstash.com"; @@ -55,7 +55,7 @@ mod tests { trace(); clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let encrypted_jsonb = serde_json::json!({"key": "value"}); @@ -94,7 +94,7 @@ mod tests { let plaintext_json = serde_json::json!({"key": "value"}); // Insert through proxy (should encrypt) - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "INSERT INTO encrypted (id, encrypted_jsonb) VALUES ($1, $2)"; client.query(sql, &[&id, &plaintext_json]).await.unwrap(); @@ -136,7 +136,7 @@ mod tests { async fn map_repeated_literals_different_columns_regression() { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); @@ -158,7 +158,7 @@ mod tests { async fn map_repeated_literals_same_column_regression() { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = format!("INSERT INTO encrypted (id, encrypted_text) VALUES ({}, 'a'), ({}, 'a') RETURNING encrypted_text", random_id(), random_id()); diff --git a/packages/cipherstash-proxy-integration/src/map_match_index.rs b/packages/cipherstash-proxy-integration/src/map_match_index.rs index efc5711ef..5bb80d2ed 100644 --- a/packages/cipherstash-proxy-integration/src/map_match_index.rs +++ b/packages/cipherstash-proxy-integration/src/map_match_index.rs @@ -8,7 +8,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let encrypted_text = "hello@cipherstash.com"; diff --git a/packages/cipherstash-proxy-integration/src/map_nulls.rs b/packages/cipherstash-proxy-integration/src/map_nulls.rs index 46f635481..cc93b3d49 100644 --- a/packages/cipherstash-proxy-integration/src/map_nulls.rs +++ b/packages/cipherstash-proxy-integration/src/map_nulls.rs @@ -7,7 +7,7 @@ mod tests { async fn map_insert_null_param() { trace(); - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let encrypted_text: Option = None; @@ -30,7 +30,7 @@ mod tests { async fn map_update_null_param() { trace(); - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let encrypted_text = "hello@cipherstash.com"; @@ -68,7 +68,7 @@ mod tests { async fn map_insert_encrypted_null_literal() { trace(); - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); @@ -93,7 +93,7 @@ mod tests { async fn map_insert_null_literal_with_param() { trace(); - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let encrypted_int2: i16 = 42; @@ -125,7 +125,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let plaintext: Option = None; diff --git a/packages/cipherstash-proxy-integration/src/map_ope_index_order.rs b/packages/cipherstash-proxy-integration/src/map_ope_index_order.rs index 18a9d83d6..69c89ba22 100644 --- a/packages/cipherstash-proxy-integration/src/map_ope_index_order.rs +++ b/packages/cipherstash-proxy-integration/src/map_ope_index_order.rs @@ -66,7 +66,7 @@ mod tests { { trace(); clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let insert = format!("INSERT INTO {table} (id, {col_name}) VALUES ($1, $2)"); for idx in interleaved_indices(values.len()) { @@ -94,7 +94,7 @@ mod tests { trace(); let table = "encrypted_ope_order_nulls_last"; clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let null_insert = format!("INSERT INTO {table} (id) VALUES ($1)"); client.query(&null_insert, &[&random_id()]).await.unwrap(); @@ -121,7 +121,7 @@ mod tests { trace(); let table = "encrypted_ope_order_nulls_first"; clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let insert = format!("INSERT INTO {table} (id, encrypted_text) VALUES ($1, $2), ($3, $4)"); client diff --git a/packages/cipherstash-proxy-integration/src/map_ope_index_where.rs b/packages/cipherstash-proxy-integration/src/map_ope_index_where.rs index cd21af86b..a0f1ef07c 100644 --- a/packages/cipherstash-proxy-integration/src/map_ope_index_where.rs +++ b/packages/cipherstash-proxy-integration/src/map_ope_index_where.rs @@ -63,7 +63,7 @@ mod tests { clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; // Insert test data let sql = format!("INSERT INTO {table} (id, {col_name}) VALUES ($1, $2)"); diff --git a/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs b/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs index 3a1ac6ccb..c7ec54313 100644 --- a/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs +++ b/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs @@ -9,7 +9,7 @@ mod tests { trace(); let table = "encrypted_ore_order_text"; clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; ore_order_helpers::ore_order_text(&client, table).await; } @@ -18,7 +18,7 @@ mod tests { trace(); let table = "encrypted_ore_order_text_desc"; clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; ore_order_helpers::ore_order_text_desc(&client, table).await; } @@ -27,7 +27,7 @@ mod tests { trace(); let table = "encrypted_ore_order_nulls_last"; clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; ore_order_helpers::ore_order_nulls_last_by_default(&client, table).await; } @@ -36,7 +36,7 @@ mod tests { trace(); let table = "encrypted_ore_order_nulls_first"; clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; ore_order_helpers::ore_order_nulls_first(&client, table).await; } @@ -45,7 +45,7 @@ mod tests { trace(); let table = "encrypted_ore_order_qualified"; clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; ore_order_helpers::ore_order_qualified_column(&client, table).await; } @@ -54,7 +54,7 @@ mod tests { trace(); let table = "encrypted_ore_order_qualified_alias"; clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; ore_order_helpers::ore_order_qualified_column_with_alias(&client, table).await; } @@ -63,7 +63,7 @@ mod tests { trace(); let table = "encrypted_ore_order_no_select_projection"; clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; ore_order_helpers::ore_order_no_eql_column_in_select_projection(&client, table).await; } @@ -72,7 +72,7 @@ mod tests { trace(); let table = "encrypted_ore_order_plaintext_column"; clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; ore_order_helpers::ore_order_plaintext_column(&client, table).await; } @@ -81,7 +81,7 @@ mod tests { trace(); let table = "encrypted_ore_order_plaintext_and_eql"; clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; ore_order_helpers::ore_order_plaintext_and_eql_columns(&client, table).await; } @@ -90,7 +90,7 @@ mod tests { trace(); let table = "encrypted_ore_order_simple_protocol"; clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; ore_order_helpers::ore_order_simple_protocol(&client, table).await; } @@ -99,7 +99,7 @@ mod tests { trace(); let table = "encrypted_ore_order_int2"; clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let values: Vec = vec![-100, -10, -1, 0, 1, 5, 10, 20, 100, 200]; ore_order_helpers::ore_order_generic( &client, @@ -116,7 +116,7 @@ mod tests { trace(); let table = "encrypted_ore_order_int2_desc"; clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let values: Vec = vec![-100, -10, -1, 0, 1, 5, 10, 20, 100, 200]; ore_order_helpers::ore_order_generic( &client, @@ -133,7 +133,7 @@ mod tests { trace(); let table = "encrypted_ore_order_int4"; clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let values: Vec = vec![ -50_000, -1_000, -1, 0, 1, 42, 1_000, 10_000, 50_000, 100_000, ]; @@ -152,7 +152,7 @@ mod tests { trace(); let table = "encrypted_ore_order_int4_desc"; clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let values: Vec = vec![ -50_000, -1_000, -1, 0, 1, 42, 1_000, 10_000, 50_000, 100_000, ]; @@ -171,7 +171,7 @@ mod tests { trace(); let table = "encrypted_ore_order_int8"; clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let values: Vec = vec![ -1_000_000, -10_000, -1, 0, 1, 42, 10_000, 100_000, 1_000_000, 9_999_999, ]; @@ -190,7 +190,7 @@ mod tests { trace(); let table = "encrypted_ore_order_int8_desc"; clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let values: Vec = vec![ -1_000_000, -10_000, -1, 0, 1, 42, 10_000, 100_000, 1_000_000, 9_999_999, ]; @@ -209,7 +209,7 @@ mod tests { trace(); let table = "encrypted_ore_order_float8"; clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let values: Vec = vec![ -99.9, -1.5, -0.001, 0.0, 0.001, 1.5, 3.25, 42.0, 99.9, 1000.5, ]; @@ -228,7 +228,7 @@ mod tests { trace(); let table = "encrypted_ore_order_float8_desc"; clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let values: Vec = vec![ -99.9, -1.5, -0.001, 0.0, 0.001, 1.5, 3.25, 42.0, 99.9, 1000.5, ]; diff --git a/packages/cipherstash-proxy-integration/src/map_ore_index_where.rs b/packages/cipherstash-proxy-integration/src/map_ore_index_where.rs index c8bf87151..9f25b9e9c 100644 --- a/packages/cipherstash-proxy-integration/src/map_ore_index_where.rs +++ b/packages/cipherstash-proxy-integration/src/map_ore_index_where.rs @@ -63,7 +63,7 @@ mod tests { clear_table(table).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; // Insert test data let sql = format!("INSERT INTO {table} (id, {col_name}) VALUES ($1, $2)"); diff --git a/packages/cipherstash-proxy-integration/src/map_params.rs b/packages/cipherstash-proxy-integration/src/map_params.rs index d8c84fde8..6fd2127ec 100644 --- a/packages/cipherstash-proxy-integration/src/map_params.rs +++ b/packages/cipherstash-proxy-integration/src/map_params.rs @@ -8,7 +8,7 @@ mod tests { reset_schema().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let plaintext = "hello@cipherstash.com"; diff --git a/packages/cipherstash-proxy-integration/src/map_unique_index.rs b/packages/cipherstash-proxy-integration/src/map_unique_index.rs index d1fc800e6..4848073b2 100644 --- a/packages/cipherstash-proxy-integration/src/map_unique_index.rs +++ b/packages/cipherstash-proxy-integration/src/map_unique_index.rs @@ -9,7 +9,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let encrypted_text = "hello@cipherstash.com"; @@ -38,7 +38,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let encrypted_int2: i16 = 42; @@ -66,7 +66,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let encrypted_int4: i32 = 42; @@ -94,7 +94,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let encrypted_int8: i64 = 42; @@ -122,7 +122,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let encrypted_float8: f64 = 42.00; @@ -150,7 +150,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let encrypted_date = NaiveDate::parse_from_str("2025-01-01", "%Y-%m-%d").unwrap(); @@ -178,7 +178,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let plaintext = "hello@cipherstash.com"; @@ -203,7 +203,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let plaintext = "hello@cipherstash.com"; diff --git a/packages/cipherstash-proxy-integration/src/migrate/mod.rs b/packages/cipherstash-proxy-integration/src/migrate/mod.rs index 701f37310..bfe17c391 100644 --- a/packages/cipherstash-proxy-integration/src/migrate/mod.rs +++ b/packages/cipherstash-proxy-integration/src/migrate/mod.rs @@ -55,7 +55,7 @@ mod tests { } }; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; for _ in 1..10 { let id = random_id(); diff --git a/packages/cipherstash-proxy-integration/src/multitenant/contention.rs b/packages/cipherstash-proxy-integration/src/multitenant/contention.rs index 7783602c8..2c15c6163 100644 --- a/packages/cipherstash-proxy-integration/src/multitenant/contention.rs +++ b/packages/cipherstash-proxy-integration/src/multitenant/contention.rs @@ -39,7 +39,7 @@ mod tests { /// Establish a connection and set the keyset for a tenant. /// Returns the ready-to-use client (connection setup is excluded from timing). async fn connect_as_tenant(keyset_id: &str) -> tokio_postgres::Client { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; // SET doesn't support parameterized values; keyset_id is from trusted env vars let sql = format!("SET CIPHERSTASH.KEYSET_ID = '{keyset_id}'"); client.execute(&sql, &[]).await.unwrap(); diff --git a/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs b/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs index 0a8dcbe42..695e39c29 100644 --- a/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs +++ b/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs @@ -17,7 +17,7 @@ mod tests { async fn connect_as_tenant(keyset_id: &str) -> tokio_postgres::Client { uuid::Uuid::parse_str(keyset_id) .unwrap_or_else(|_| panic!("invalid UUID for keyset_id: {keyset_id}")); - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = format!("SET CIPHERSTASH.KEYSET_ID = '{keyset_id}'"); client.execute(&sql, &[]).await.unwrap(); client diff --git a/packages/cipherstash-proxy-integration/src/multitenant/set_keyset_id.rs b/packages/cipherstash-proxy-integration/src/multitenant/set_keyset_id.rs index d6037013b..f76f221ce 100644 --- a/packages/cipherstash-proxy-integration/src/multitenant/set_keyset_id.rs +++ b/packages/cipherstash-proxy-integration/src/multitenant/set_keyset_id.rs @@ -33,7 +33,7 @@ mod tests { // KEYSET_ID IS SCOPED TO A CONNECTION // The same client/connection is used for tests - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; // DEFAULT_KEYSET_ID SHOULD BE DISABLED FOR THIS TEST // SET KEYSET IS REQUIRED @@ -103,7 +103,7 @@ mod tests { // KEYSET_ID IS SCOPED TO A CONNECTION // The same client/connection is used for tests - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; // DEFAULT_KEYSET_ID SHOULD BE DISABLED FOR THIS TEST // SET KEYSET IS REQUIRED @@ -190,8 +190,8 @@ mod tests { let tenant_1_text = "TENANT_1".to_string(); let tenant_2_text = "TENANT_2".to_string(); - let tenant_1_client = connect_with_tls(PROXY).await; - let tenant_2_client = connect_with_tls(PROXY).await; + let tenant_1_client = connect_with_tls(*PROXY).await; + let tenant_2_client = connect_with_tls(*PROXY).await; // DEFAULT_KEYSET_ID SHOULD BE DISABLED FOR THIS TEST // SET KEYSET IS REQUIRED @@ -262,7 +262,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let tenant_keyset_id_1 = std::env::var("CS_TENANT_KEYSET_ID_1") .map(|s| Uuid::parse_str(&s).unwrap()) @@ -308,7 +308,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let tenant_keyset_id_1 = std::env::var("CS_TENANT_KEYSET_ID_1") .map(|s| Uuid::parse_str(&s).unwrap()) @@ -346,7 +346,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let tenant_keyset_id_1 = std::env::var("CS_TENANT_KEYSET_ID_1") .map(|s| Uuid::parse_str(&s).unwrap()) diff --git a/packages/cipherstash-proxy-integration/src/multitenant/set_keyset_name.rs b/packages/cipherstash-proxy-integration/src/multitenant/set_keyset_name.rs index 44afdbb62..0d9af1757 100644 --- a/packages/cipherstash-proxy-integration/src/multitenant/set_keyset_name.rs +++ b/packages/cipherstash-proxy-integration/src/multitenant/set_keyset_name.rs @@ -28,7 +28,7 @@ mod tests { // KEYSET_NAME IS SCOPED TO A CONNECTION // The same client/connection is used for tests - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; // DEFAULT_KEYSET_ID SHOULD BE DISABLED FOR THIS TEST // SET KEYSET IS REQUIRED @@ -93,7 +93,7 @@ mod tests { // KEYSET_ID IS SCOPED TO A CONNECTION // The same client/connection is used for tests - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; // DEFAULT_KEYSET_ID SHOULD BE DISABLED FOR THIS TEST // SET KEYSET IS REQUIRED @@ -181,8 +181,8 @@ mod tests { let tenant_1_text = "TENANT_1".to_string(); let tenant_2_text = "TENANT_2".to_string(); - let tenant_1_client = connect_with_tls(PROXY).await; - let tenant_2_client = connect_with_tls(PROXY).await; + let tenant_1_client = connect_with_tls(*PROXY).await; + let tenant_2_client = connect_with_tls(*PROXY).await; // DEFAULT_KEYSET_ID SHOULD BE DISABLED FOR THIS TEST // SET KEYSET IS REQUIRED @@ -263,8 +263,8 @@ mod tests { let tenant_1_text = "TENANT_1_DATA".to_string(); let tenant_2_text = "TENANT_2_DATA".to_string(); - let tenant_1_client = connect_with_tls(PROXY).await; - let tenant_2_client = connect_with_tls(PROXY).await; + let tenant_1_client = connect_with_tls(*PROXY).await; + let tenant_2_client = connect_with_tls(*PROXY).await; // Set tenant keysets for each client let sql = format!("SET CIPHERSTASH.KEYSET_NAME = '{tenant_keyset_name_1}'"); @@ -327,7 +327,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let tenant_keyset_name_1 = std::env::var("CS_TENANT_KEYSET_NAME_1").unwrap(); @@ -372,7 +372,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let tenant_keyset_name_1 = std::env::var("CS_TENANT_KEYSET_NAME_1").unwrap(); diff --git a/packages/cipherstash-proxy-integration/src/passthrough.rs b/packages/cipherstash-proxy-integration/src/passthrough.rs index 676f94e54..397303858 100644 --- a/packages/cipherstash-proxy-integration/src/passthrough.rs +++ b/packages/cipherstash-proxy-integration/src/passthrough.rs @@ -6,7 +6,7 @@ mod tests { #[tokio::test] async fn passthrough_statement() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; clear().await; @@ -29,7 +29,7 @@ mod tests { #[tokio::test] async fn passthrough_invalid_statement() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; clear().await; @@ -56,7 +56,7 @@ mod tests { async fn passthrough_statement_parallel() { for _x in 1..100 { tokio::spawn(async move { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; for _x in 1..10 { let id = random_id(); @@ -86,7 +86,7 @@ mod tests { #[tokio::test] async fn passthrough_insert_from_select() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; clear().await; @@ -118,7 +118,7 @@ mod tests { #[tokio::test] async fn passthrough_insert_with_value_from_select() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; clear().await; @@ -149,7 +149,7 @@ mod tests { #[tokio::test] async fn passthrough_insert_with_returning() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; clear().await; @@ -170,7 +170,7 @@ mod tests { #[tokio::test] async fn passthrough_select_with_cardinality() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; clear().await; @@ -198,7 +198,7 @@ mod tests { #[tokio::test] async fn passthrough_delete_with_select() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; clear().await; diff --git a/packages/cipherstash-proxy-integration/src/pipeline.rs b/packages/cipherstash-proxy-integration/src/pipeline.rs index d1b1ea339..f89f5c0b5 100644 --- a/packages/cipherstash-proxy-integration/src/pipeline.rs +++ b/packages/cipherstash-proxy-integration/src/pipeline.rs @@ -16,7 +16,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let counter = AtomicUsize::new(0); diff --git a/packages/cipherstash-proxy-integration/src/schema_change.rs b/packages/cipherstash-proxy-integration/src/schema_change.rs index 7c7e2f823..ff1fa0420 100644 --- a/packages/cipherstash-proxy-integration/src/schema_change.rs +++ b/packages/cipherstash-proxy-integration/src/schema_change.rs @@ -4,7 +4,7 @@ mod tests { #[tokio::test] async fn schema_change_reloads_schema() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); diff --git a/packages/cipherstash-proxy-integration/src/select/distinct_order_by.rs b/packages/cipherstash-proxy-integration/src/select/distinct_order_by.rs index 2ae56db1c..c3517d56c 100644 --- a/packages/cipherstash-proxy-integration/src/select/distinct_order_by.rs +++ b/packages/cipherstash-proxy-integration/src/select/distinct_order_by.rs @@ -45,7 +45,7 @@ mod tests { insert_text(&["cherry", "apple", "date", "banana"]).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SELECT DISTINCT encrypted_text FROM encrypted ORDER BY encrypted_text ASC"; let rows = client.query(sql, &[]).await.unwrap(); @@ -78,7 +78,7 @@ mod tests { // Six rows, three distinct plaintexts. insert_text(&["cherry", "apple", "banana", "apple", "cherry", "apple"]).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; // Without ORDER BY: deduplicated in place, no subquery wrapping. let sql = "SELECT DISTINCT encrypted_text FROM encrypted"; @@ -108,7 +108,7 @@ mod tests { insert_text(&["cherry", "apple"]).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SELECT DISTINCT id, encrypted_text FROM encrypted ORDER BY encrypted_text"; let rows = client.query(sql, &[]).await.unwrap(); @@ -130,7 +130,7 @@ mod tests { insert_text(&["cherry", "apple"]).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SELECT DISTINCT encrypted_text AS fruit FROM encrypted ORDER BY encrypted_text"; let rows = client.query(sql, &[]).await.unwrap(); @@ -166,7 +166,7 @@ mod tests { .await; } - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SELECT DISTINCT plaintext, encrypted_text FROM encrypted \ ORDER BY plaintext, encrypted_text"; @@ -204,7 +204,7 @@ mod tests { .await; } - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; // `1` is `plaintext`. let sql = "SELECT DISTINCT plaintext, encrypted_text FROM encrypted \ @@ -235,7 +235,7 @@ mod tests { insert_text(&["cherry", "apple", "date", "banana"]).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SELECT DISTINCT encrypted_text FROM encrypted \ ORDER BY encrypted_text ASC LIMIT 2"; diff --git a/packages/cipherstash-proxy-integration/src/select/indexing.rs b/packages/cipherstash-proxy-integration/src/select/indexing.rs index 654706d75..1870a34a3 100644 --- a/packages/cipherstash-proxy-integration/src/select/indexing.rs +++ b/packages/cipherstash-proxy-integration/src/select/indexing.rs @@ -32,7 +32,7 @@ mod tests { insert(&sql, &[&id, &encrypted_text]).await; } - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "CREATE INDEX ON encrypted (eql_v3.ord_term(encrypted_text))"; let _ = client.simple_query(sql).await; diff --git a/packages/cipherstash-proxy-integration/src/select/jsonb_array_elements.rs b/packages/cipherstash-proxy-integration/src/select/jsonb_array_elements.rs index 525fd548b..f0f30d8d9 100644 --- a/packages/cipherstash-proxy-integration/src/select/jsonb_array_elements.rs +++ b/packages/cipherstash-proxy-integration/src/select/jsonb_array_elements.rs @@ -27,7 +27,7 @@ mod tests { #[tokio::test] async fn select_jsonb_array_elements_with_string() { trace(); - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; clear_with_client(&client).await; insert_jsonb_with_client(&client).await; @@ -39,7 +39,7 @@ mod tests { #[tokio::test] async fn select_jsonb_array_elements_with_numeric() { trace(); - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; clear_with_client(&client).await; insert_jsonb_with_client(&client).await; @@ -51,7 +51,7 @@ mod tests { #[tokio::test] async fn select_jsonb_array_elements_with_unknown_field() { trace(); - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; clear_with_client(&client).await; insert_jsonb_with_client(&client).await; diff --git a/packages/cipherstash-proxy-integration/src/select/jsonb_containment_index.rs b/packages/cipherstash-proxy-integration/src/select/jsonb_containment_index.rs index fe9762bf8..3f5383f59 100644 --- a/packages/cipherstash-proxy-integration/src/select/jsonb_containment_index.rs +++ b/packages/cipherstash-proxy-integration/src/select/jsonb_containment_index.rs @@ -203,7 +203,7 @@ mod tests { trace(); ensure_fixture_data().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let test_case = ContainmentTestCase::new(OperandType::$lhs, OperandType::$rhs); let search_value = test_case.search_value(); test_case.run(&client, &search_value).await; @@ -217,7 +217,7 @@ mod tests { /// Does NOT call clear() - preserves data from other tests. /// Only inserts if the fixture data is missing. async fn ensure_fixture_data() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; // Check if fixture data already exists let sql = format!( @@ -303,7 +303,7 @@ mod tests { trace(); ensure_fixture_data().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; // Use extended query protocol with parameterized query // Filter by fixture ID range to isolate from other test data diff --git a/packages/cipherstash-proxy-integration/src/select/jsonb_fusion_gaps.rs b/packages/cipherstash-proxy-integration/src/select/jsonb_fusion_gaps.rs index 5cc110c1f..3130a48ba 100644 --- a/packages/cipherstash-proxy-integration/src/select/jsonb_fusion_gaps.rs +++ b/packages/cipherstash-proxy-integration/src/select/jsonb_fusion_gaps.rs @@ -57,7 +57,7 @@ mod tests { clear().await; let id = insert_nested().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SELECT id FROM encrypted WHERE encrypted_jsonb -> 'nested' -> 'string' = $1"; let rows = client @@ -89,7 +89,7 @@ mod tests { clear().await; insert_nested().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let selector: Option = None; let sql = "SELECT id FROM encrypted WHERE encrypted_jsonb -> $1 = $2"; diff --git a/packages/cipherstash-proxy-integration/src/select/jsonb_path_query.rs b/packages/cipherstash-proxy-integration/src/select/jsonb_path_query.rs index 81bba3572..80abf6c52 100644 --- a/packages/cipherstash-proxy-integration/src/select/jsonb_path_query.rs +++ b/packages/cipherstash-proxy-integration/src/select/jsonb_path_query.rs @@ -34,7 +34,7 @@ mod tests { #[tokio::test] async fn select_jsonb_path_query_number() { trace(); - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; clear_with_client(&client).await; insert_jsonb_with_client(&client).await; @@ -45,7 +45,7 @@ mod tests { #[tokio::test] async fn select_jsonb_path_query_string() { trace(); - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; clear_with_client(&client).await; insert_jsonb_with_client(&client).await; @@ -56,7 +56,7 @@ mod tests { #[tokio::test] async fn select_jsonb_path_query_value() { trace(); - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; clear_with_client(&client).await; insert_jsonb_with_client(&client).await; @@ -72,7 +72,7 @@ mod tests { #[tokio::test] async fn select_jsonb_path_query_with_unknown() { trace(); - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; clear_with_client(&client).await; insert_jsonb_with_client(&client).await; @@ -97,7 +97,7 @@ mod tests { #[tokio::test] async fn select_jsonb_path_query_with_alias() { trace(); - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; clear_with_client(&client).await; insert_jsonb_with_client(&client).await; diff --git a/packages/cipherstash-proxy-integration/src/select/jsonb_selector_param_types.rs b/packages/cipherstash-proxy-integration/src/select/jsonb_selector_param_types.rs index b2d3e5863..04971ee9e 100644 --- a/packages/cipherstash-proxy-integration/src/select/jsonb_selector_param_types.rs +++ b/packages/cipherstash-proxy-integration/src/select/jsonb_selector_param_types.rs @@ -22,7 +22,7 @@ mod tests { clear().await; insert_jsonb().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let selector = JsonPath::new("$.number"); let sql = "SELECT jsonb_path_exists(encrypted_jsonb, $1) FROM encrypted"; @@ -42,7 +42,7 @@ mod tests { clear().await; insert_jsonb().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let selector = JsonPath::new("$.string"); let sql = "SELECT jsonb_path_query_first(encrypted_jsonb, $1) FROM encrypted"; @@ -61,7 +61,7 @@ mod tests { clear().await; insert_jsonb().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SELECT encrypted_jsonb -> $1 FROM encrypted"; let stmt = client diff --git a/packages/cipherstash-proxy-integration/src/select/operator_backed_predicates.rs b/packages/cipherstash-proxy-integration/src/select/operator_backed_predicates.rs index a8112d976..62cd88706 100644 --- a/packages/cipherstash-proxy-integration/src/select/operator_backed_predicates.rs +++ b/packages/cipherstash-proxy-integration/src/select/operator_backed_predicates.rs @@ -39,7 +39,7 @@ mod tests { insert_rows(&[("a", 1), ("b", 2), ("c", 3), ("d", 4), ("e", 5)]).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SELECT encrypted_int4 FROM encrypted WHERE encrypted_int4 BETWEEN 2 AND 4 \ ORDER BY encrypted_int4"; @@ -65,7 +65,7 @@ mod tests { insert_rows(&[("apple", 1), ("banana", 2), ("cherry", 3)]).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SELECT encrypted_text FROM encrypted WHERE encrypted_text IS DISTINCT FROM 'apple'"; diff --git a/packages/cipherstash-proxy-integration/src/select/operator_class_shapes.rs b/packages/cipherstash-proxy-integration/src/select/operator_class_shapes.rs index dfe188af6..e3163670a 100644 --- a/packages/cipherstash-proxy-integration/src/select/operator_class_shapes.rs +++ b/packages/cipherstash-proxy-integration/src/select/operator_class_shapes.rs @@ -62,7 +62,7 @@ mod tests { clear().await; insert_fixture().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SELECT DISTINCT ON (encrypted_text) encrypted_text FROM encrypted"; let rows = client.query(sql, &[]).await.unwrap(); @@ -81,7 +81,7 @@ mod tests { clear().await; insert_fixture().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SELECT encrypted_text FROM encrypted ORDER BY 1"; let rows = client.query(sql, &[]).await.unwrap(); @@ -97,7 +97,7 @@ mod tests { clear().await; insert_fixture().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SELECT encrypted_text FROM encrypted GROUP BY 1"; let rows = client.query(sql, &[]).await.unwrap(); @@ -113,7 +113,7 @@ mod tests { clear().await; insert_fixture().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; // Two 'apple' rows and two 'banana' rows, so each of those partitions // must produce a rank 2. Every rank being 1 means no partitioning. @@ -140,7 +140,7 @@ mod tests { clear().await; insert_fixture().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SELECT encrypted_text FROM encrypted UNION ALL SELECT encrypted_text FROM encrypted"; diff --git a/packages/cipherstash-proxy-integration/src/select/pg_catalog.rs b/packages/cipherstash-proxy-integration/src/select/pg_catalog.rs index e8c752a62..79320444c 100644 --- a/packages/cipherstash-proxy-integration/src/select/pg_catalog.rs +++ b/packages/cipherstash-proxy-integration/src/select/pg_catalog.rs @@ -8,7 +8,7 @@ mod tests { /// #[tokio::test] async fn select_from_pg_catalog() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SELECT attname, atttypid FROM pg_catalog.pg_attribute WHERE attrelid IS NOT NULL AND NOT attisdropped AND attnum > 0 ORDER BY attnum"; let rows = client.query(sql, &[]).await.unwrap(); diff --git a/packages/cipherstash-proxy-integration/src/select/select_where_in.rs b/packages/cipherstash-proxy-integration/src/select/select_where_in.rs index 5e6119a86..90cd04bb5 100644 --- a/packages/cipherstash-proxy-integration/src/select/select_where_in.rs +++ b/packages/cipherstash-proxy-integration/src/select/select_where_in.rs @@ -58,7 +58,7 @@ mod tests { let sql = "SELECT encrypted_text FROM encrypted WHERE encrypted_text IN ('apple', 'banana')"; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let rows = client.query(sql, &[]).await.unwrap(); let actual: Vec = rows.iter().map(|r| r.get("encrypted_text")).collect(); assert_eq!(vec!["apple", "banana"], sorted(actual)); @@ -78,7 +78,7 @@ mod tests { let sql = "SELECT encrypted_text FROM encrypted WHERE encrypted_text NOT IN ('apple', 'banana')"; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let rows = client.query(sql, &[]).await.unwrap(); let actual: Vec = rows.iter().map(|r| r.get("encrypted_text")).collect(); assert_eq!(vec!["cherry"], actual); @@ -96,7 +96,7 @@ mod tests { insert_text(&["apple", "banana", "cherry"]).await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SELECT encrypted_text FROM encrypted WHERE encrypted_text IN ($1, $2)"; let rows = client @@ -127,7 +127,7 @@ mod tests { let sql = "SELECT encrypted_text FROM encrypted WHERE encrypted_text IN ('durian')"; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let rows = client.query(sql, &[]).await.unwrap(); assert!(rows.is_empty()); diff --git a/packages/cipherstash-proxy-integration/src/select/select_where_jsonb.rs b/packages/cipherstash-proxy-integration/src/select/select_where_jsonb.rs index 86f0a80e3..4a23a4e83 100644 --- a/packages/cipherstash-proxy-integration/src/select/select_where_jsonb.rs +++ b/packages/cipherstash-proxy-integration/src/select/select_where_jsonb.rs @@ -63,7 +63,7 @@ mod tests { insert_jsonb().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let selector = "number"; let value = Value::from(1); diff --git a/packages/cipherstash-proxy-integration/src/select/unmappable.rs b/packages/cipherstash-proxy-integration/src/select/unmappable.rs index 32686bc67..194a3cd43 100644 --- a/packages/cipherstash-proxy-integration/src/select/unmappable.rs +++ b/packages/cipherstash-proxy-integration/src/select/unmappable.rs @@ -11,7 +11,7 @@ mod tests { /// #[tokio::test] async fn unmappable_table_not_found() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SELECT blah FROM vtha"; let result = client.query(sql, &[]).await; @@ -24,7 +24,7 @@ mod tests { #[tokio::test] async fn unmappable_column_not_found() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SELECT blah FROM encrypted"; let result = client.query(sql, &[]).await; @@ -37,7 +37,7 @@ mod tests { #[tokio::test] async fn unmappable_native_cannot_be_unified_with_encrypted() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SELECT * FROM encrypted WHERE plaintext = encrypted_text"; let result = client.query(sql, &[]).await; @@ -50,7 +50,7 @@ mod tests { #[tokio::test] async fn unmappable_syntax_error() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SELECT *, FROM encrypted"; let result = client.query(sql, &[]).await; diff --git a/packages/cipherstash-proxy-integration/src/set_keyset_error.rs b/packages/cipherstash-proxy-integration/src/set_keyset_error.rs index e4b75f9b7..30f636335 100644 --- a/packages/cipherstash-proxy-integration/src/set_keyset_error.rs +++ b/packages/cipherstash-proxy-integration/src/set_keyset_error.rs @@ -28,7 +28,7 @@ mod tests { async fn set_keyset_id_with_default_config_error() { trace(); - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SET CIPHERSTASH.KEYSET_ID = '2cace9db-3a2a-4b46-a184-ba412b3e0730'"; @@ -49,7 +49,7 @@ mod tests { async fn set_keyset_name_with_default_config_error() { trace(); - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let sql = "SET CIPHERSTASH.KEYSET_NAME = 'tenant-1'"; diff --git a/packages/cipherstash-proxy-integration/src/simple_protocol/error_handling.rs b/packages/cipherstash-proxy-integration/src/simple_protocol/error_handling.rs index 5871c8c04..9e1a92145 100644 --- a/packages/cipherstash-proxy-integration/src/simple_protocol/error_handling.rs +++ b/packages/cipherstash-proxy-integration/src/simple_protocol/error_handling.rs @@ -4,7 +4,7 @@ mod tests { #[tokio::test] async fn frontend_error_does_not_crash_connection() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; // Statement has the wrong column name let sql = format!( diff --git a/packages/cipherstash-proxy-integration/src/simple_protocol/map_literals.rs b/packages/cipherstash-proxy-integration/src/simple_protocol/map_literals.rs index 35d7328ad..fe3257358 100644 --- a/packages/cipherstash-proxy-integration/src/simple_protocol/map_literals.rs +++ b/packages/cipherstash-proxy-integration/src/simple_protocol/map_literals.rs @@ -6,7 +6,7 @@ mod tests { #[tokio::test] async fn simple_protocol_without_encryption() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let sql = format!("INSERT INTO encrypted (id, plaintext) VALUES ({id}, 'plain')"); client @@ -28,7 +28,7 @@ mod tests { #[tokio::test] async fn simple_protocol_text() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let encrypted_text = "hello@cipherstash.com"; @@ -68,7 +68,7 @@ mod tests { #[tokio::test] async fn simple_protocol_int2() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let encrypted_int2: i16 = 42; @@ -108,7 +108,7 @@ mod tests { #[tokio::test] async fn simple_protocol_date() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let encrypted_date = NaiveDate::parse_from_str("2025-01-01", "%Y-%m-%d").unwrap(); @@ -149,7 +149,7 @@ mod tests { #[tokio::test] async fn simple_protocol_date_with_iso() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let encrypted_date = @@ -191,7 +191,7 @@ mod tests { #[tokio::test] async fn simple_protocol_int4() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let encrypted_int4: i32 = 42; @@ -243,7 +243,7 @@ mod tests { #[tokio::test] async fn frontend_error_does_not_crash_connection() { - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; // Statement has the wrong column name let sql = format!( diff --git a/packages/cipherstash-proxy-integration/src/simple_protocol/map_nulls.rs b/packages/cipherstash-proxy-integration/src/simple_protocol/map_nulls.rs index 78d93a8e0..07821283b 100644 --- a/packages/cipherstash-proxy-integration/src/simple_protocol/map_nulls.rs +++ b/packages/cipherstash-proxy-integration/src/simple_protocol/map_nulls.rs @@ -9,7 +9,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); let encrypted_text: Option<&str> = None; @@ -51,7 +51,7 @@ mod tests { clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let id = random_id(); diff --git a/packages/cipherstash-proxy-integration/src/simple_protocol/multiple_statements.rs b/packages/cipherstash-proxy-integration/src/simple_protocol/multiple_statements.rs index d1a20fa97..c834ac910 100644 --- a/packages/cipherstash-proxy-integration/src/simple_protocol/multiple_statements.rs +++ b/packages/cipherstash-proxy-integration/src/simple_protocol/multiple_statements.rs @@ -9,7 +9,7 @@ mod tests { trace(); clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let data = (0..5) .map(|_| (random_id(), Faker.fake::())) @@ -53,7 +53,7 @@ mod tests { trace(); clear().await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let data = (0..5) .map(|_| (random_id(), Faker.fake::(), Faker.fake::())) diff --git a/packages/cipherstash-proxy-integration/src/update/update_domain_type.rs b/packages/cipherstash-proxy-integration/src/update/update_domain_type.rs index 6fceb42f2..c55a5ad54 100644 --- a/packages/cipherstash-proxy-integration/src/update/update_domain_type.rs +++ b/packages/cipherstash-proxy-integration/src/update/update_domain_type.rs @@ -54,7 +54,7 @@ mod tests { // Then update with RETURNING clause let sql = "UPDATE encrypted SET plaintext_domain = $1, encrypted_text = $2 WHERE id = $3 RETURNING id, plaintext_domain, encrypted_text"; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; let result = client .query(sql, &[&updated_domain, &updated_text, &id]) .await diff --git a/packages/cipherstash-proxy-integration/src/update/update_with_reused_param.rs b/packages/cipherstash-proxy-integration/src/update/update_with_reused_param.rs index b8afe1dfe..b4503f8d2 100644 --- a/packages/cipherstash-proxy-integration/src/update/update_with_reused_param.rs +++ b/packages/cipherstash-proxy-integration/src/update/update_with_reused_param.rs @@ -30,7 +30,7 @@ mod tests { ) .await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; // The same placeholder is the stored value and the predicate operand. let sql = "UPDATE encrypted SET encrypted_text = $1 WHERE encrypted_text = $1"; @@ -59,7 +59,7 @@ mod tests { ) .await; - let client = connect_with_tls(PROXY).await; + let client = connect_with_tls(*PROXY).await; // `$1` stores, `$2` queries; the reverse of the pairing above. let updated = "goodbye@cipherstash.com".to_string(); From 251408e17dbc13f6a73320ab20aa739254f80a64 Mon Sep 17 00:00:00 2001 From: James Sadler Date: Wed, 29 Jul 2026 23:47:09 +1000 Subject: [PATCH 2/4] wip(CIP-3680): remove the mapping-error escape hatch Checkpoint of in-progress work. Removes CS_DEVELOPMENT__ENABLE_MAPPING_ERRORS and makes a type-check failure fatal when the statement touches an encrypted table, with a new eql_mapper::may_touch_eql_columns to make that distinction. Unverified: does not necessarily compile or pass. Committed so the work survives an interruption. --- .../src/select/unmappable.rs | 196 +++++++++++++++++- .../cipherstash-proxy/src/config/tandem.rs | 10 - packages/cipherstash-proxy/src/main.rs | 4 - .../src/postgresql/context/mod.rs | 4 - .../src/postgresql/frontend.rs | 52 ++++- packages/eql-mapper/src/eql_mapper.rs | 67 +++++- packages/eql-mapper/src/lib.rs | 132 +++++++++++- tests/mise.tcp.toml | 1 - tests/mise.tls.toml | 1 - 9 files changed, 428 insertions(+), 39 deletions(-) diff --git a/packages/cipherstash-proxy-integration/src/select/unmappable.rs b/packages/cipherstash-proxy-integration/src/select/unmappable.rs index 194a3cd43..12c954065 100644 --- a/packages/cipherstash-proxy-integration/src/select/unmappable.rs +++ b/packages/cipherstash-proxy-integration/src/select/unmappable.rs @@ -1,13 +1,14 @@ #[cfg(test)] mod tests { - use crate::common::{connect_with_tls, PROXY}; + use crate::common::{clear, connect_with_tls, get_database_port, random_id, PROXY}; + use std::error::Error; /// - /// Tests unmappble statements return an error in tests. + /// Tests that a statement Proxy cannot map is refused. /// - /// `enable_mapping_errors` should be `true` in the test configuration.` - /// - /// Test ensures that unmappable SQL statements return an error + /// There is no configuration that turns this off. A statement that fails to type check and + /// touches an encrypted column is always an error, because forwarding it produces a wrong + /// answer rather than a degraded one. /// #[tokio::test] async fn unmappable_table_not_found() { @@ -60,4 +61,189 @@ mod tests { "Expected unmappble SQL statement to return an error", ); } + + /// + /// `eql_v3_boolean` is storage-only: it carries no equality term, so `DISTINCT` cannot be keyed + /// on it and the statement fails to type check. + /// + /// Proxy used to swallow that failure and forward the statement, which returned the raw EQL + /// payloads — `{"c": "mBbK panic!( + "Expected DISTINCT on a storage-only encrypted column to be refused, \ + but Proxy returned {} row(s) of raw ciphertext", + rows.len() + ), + Err(err) => err, + }; + + let message = match err.source() { + Some(db_error) => db_error.to_string(), + None => err.to_string(), + }; + + assert!( + message.contains("could not be type checked"), + "Expected a mapping error, got: {message}" + ); + } + + /// + /// The strongest form of the assertion: a statement Proxy refuses must never reach PostgreSQL. + /// + /// This `INSERT` selects a native `text` column into an encrypted column, which cannot be + /// unified. Proxy used to forward it unmapped, sending the value to the database without + /// encrypting it. The check is made against PostgreSQL *directly*, bypassing Proxy, so nothing + /// in the read path can disguise a write that landed. + /// + #[tokio::test] + async fn unmappable_write_to_an_encrypted_column_never_reaches_postgres() { + clear().await; + + let client = connect_with_tls(*PROXY).await; + + let id = random_id(); + let plaintext = "hello@cipherstash.com"; + + let sql = "INSERT INTO plaintext (id, plaintext) VALUES ($1, $2)"; + client.query(sql, &[&id, &plaintext]).await.unwrap(); + + // Native `plaintext.plaintext` cannot be unified with encrypted `encrypted.encrypted_text`. + let sql = "INSERT INTO encrypted (id, encrypted_text) SELECT id, plaintext FROM plaintext WHERE id = $1"; + let result = client.query(sql, &[&id]).await; + + assert!( + result.is_err(), + "Expected an unmappable write to an encrypted column to be refused", + ); + + // Ask the database itself, not Proxy. + let db = connect_with_tls(get_database_port()).await; + let rows = db + .query("SELECT encrypted_text FROM encrypted WHERE id = $1", &[&id]) + .await + .unwrap(); + + assert!( + rows.is_empty(), + "Refused statement still wrote {} row(s) to the database", + rows.len() + ); + } + + /// + /// The counterpart, and the reason the unmappable check is narrowed to statements touching an + /// encrypted column rather than made fatal outright. + /// + /// `requires_type_check` is purely syntactic, so every `SELECT` is type checked whether or not + /// encryption is involved, and the mapper's SQL coverage is narrower than PostgreSQL's. Driver + /// introspection of `pg_catalog` fails to type check (`Table not found: pg_catalog.pg_type`) + /// and essentially every PostgreSQL driver issues it. Rejecting it would break working + /// applications for no security benefit — there is no encrypted data in the statement. + /// + #[tokio::test] + async fn unmappable_statement_with_no_encrypted_columns_is_forwarded() { + let client = connect_with_tls(*PROXY).await; + + let sql = "SELECT attname, atttypid FROM pg_catalog.pg_attribute WHERE attnum > 0 LIMIT 5"; + let rows = client.query(sql, &[]).await.unwrap(); + + assert!( + !rows.is_empty(), + "Expected pg_catalog introspection to still be forwarded to the database", + ); + } + + /// + /// The same rule applied to an ordinary query over a table with no encrypted columns: + /// `ARRAY_AGG`/`CARDINALITY` cannot be typed by the mapper, but the statement is harmless. + /// + #[tokio::test] + async fn unmappable_native_only_statement_is_forwarded() { + clear().await; + + let client = connect_with_tls(*PROXY).await; + + let id = random_id(); + let sql = "INSERT INTO plaintext (id, plaintext) VALUES ($1, $2)"; + client + .query(sql, &[&id, &"hello@cipherstash.com"]) + .await + .unwrap(); + + let sql = "SELECT ARRAY_REMOVE(ARRAY_AGG(id), NULL), plaintext + FROM plaintext + WHERE CARDINALITY(ARRAY[1,2]) <> 0 + GROUP BY plaintext"; + let rows = client.query(sql, &[]).await.unwrap(); + + assert_eq!(rows.len(), 1); + } + + /// + /// A statement over a table the schema has never heard of has no encrypted columns to expose, + /// so it is forwarded and PostgreSQL rejects it with its own error rather than Proxy inventing + /// one. Clients depend on seeing the real database error. + /// + #[tokio::test] + async fn unknown_table_is_reported_by_postgres_not_proxy() { + let client = connect_with_tls(*PROXY).await; + + let sql = "SELECT * FROM blahvtha"; + let result = client.query(sql, &[]).await; + + match result { + Ok(_) => panic!("Expected an error for an unknown table"), + Err(error) => { + let db_error = error.source().unwrap().to_string(); + assert_eq!(db_error, "ERROR: relation \"blahvtha\" does not exist"); + } + } + } + + /// + /// A read that Proxy cannot map must not fall back to handing the client raw EQL payloads. + /// + #[tokio::test] + async fn unmappable_read_does_not_leak_ciphertext_to_the_client() { + clear().await; + + let client = connect_with_tls(*PROXY).await; + + let id = random_id(); + let sql = "INSERT INTO encrypted (id, encrypted_text) VALUES ($1, $2)"; + client + .query(sql, &[&id, &"hello@cipherstash.com"]) + .await + .unwrap(); + + // Native and encrypted cannot be unified, so this cannot be mapped. + let sql = "SELECT encrypted_text FROM encrypted WHERE plaintext = encrypted_text"; + let result = client.query(sql, &[]).await; + + assert!( + result.is_err(), + "Expected an unmappable read of an encrypted column to be refused rather than \ + returning raw EQL payloads", + ); + } } diff --git a/packages/cipherstash-proxy/src/config/tandem.rs b/packages/cipherstash-proxy/src/config/tandem.rs index f149144d0..5086b618a 100644 --- a/packages/cipherstash-proxy/src/config/tandem.rs +++ b/packages/cipherstash-proxy/src/config/tandem.rs @@ -64,9 +64,6 @@ pub struct DevelopmentConfig { #[serde(default)] pub disable_database_tls: bool, - - #[serde(default)] - pub enable_mapping_errors: bool, } /// Config defaults to a file called `tandem` in the current directory. @@ -245,13 +242,6 @@ impl TandemConfig { } } - pub fn mapping_errors_enabled(&self) -> bool { - match &self.development { - Some(dev) => dev.enable_mapping_errors, - None => false, - } - } - pub fn use_structured_logging(&self) -> bool { matches!(self.log.format, LogFormat::Structured) } diff --git a/packages/cipherstash-proxy/src/main.rs b/packages/cipherstash-proxy/src/main.rs index a11da143d..d94963350 100644 --- a/packages/cipherstash-proxy/src/main.rs +++ b/packages/cipherstash-proxy/src/main.rs @@ -171,10 +171,6 @@ async fn init(mut config: TandemConfig) -> Proxy { warn!(msg = "Encrypted statement mapping is not enabled"); } - if config.mapping_errors_enabled() { - info!(msg = "Encrypted statement mapping errors are enabled"); - } - let _ = rustls::crypto::aws_lc_rs::default_provider() .install_default() .inspect_err(|err| { diff --git a/packages/cipherstash-proxy/src/postgresql/context/mod.rs b/packages/cipherstash-proxy/src/postgresql/context/mod.rs index d42e015cf..d29e44f57 100644 --- a/packages/cipherstash-proxy/src/postgresql/context/mod.rs +++ b/packages/cipherstash-proxy/src/postgresql/context/mod.rs @@ -837,10 +837,6 @@ where self.config.mapping_disabled() } - pub fn mapping_errors_enabled(&self) -> bool { - self.config.mapping_errors_enabled() - } - pub fn slow_db_response_min_duration(&self) -> std::time::Duration { self.config.slow_db_response_min_duration() } diff --git a/packages/cipherstash-proxy/src/postgresql/frontend.rs b/packages/cipherstash-proxy/src/postgresql/frontend.rs index 88a3e1f0c..9e8ae3c04 100644 --- a/packages/cipherstash-proxy/src/postgresql/frontend.rs +++ b/packages/cipherstash-proxy/src/postgresql/frontend.rs @@ -434,11 +434,11 @@ where let typed_statement = match self.type_check(statement) { Ok(ts) => ts, Err(err) => { - if self.context.mapping_errors_enabled() { + if self.statement_may_touch_eql_columns(statement) { return Err(err); - } else { - return Ok(None); - }; + } + counter!(STATEMENTS_PASSTHROUGH_TOTAL).increment(1); + return Ok(None); } }; @@ -805,11 +805,11 @@ where let typed_statement = match self.type_check(&statement) { Ok(ts) => ts, Err(err) => { - if self.context.mapping_errors_enabled() { + if self.statement_may_touch_eql_columns(&statement) { return Err(err); - } else { - return Ok(None); - }; + } + counter!(STATEMENTS_PASSTHROUGH_TOTAL).increment(1); + return Ok(None); } }; @@ -1172,7 +1172,6 @@ where warn!( client_id = self.context.client_id, msg = "Internal Error in EQL Mapper", - mapping_errors_enabled = self.context.mapping_errors_enabled(), error = str, ); counter!(STATEMENTS_UNMAPPABLE_TOTAL).increment(1); @@ -1182,7 +1181,6 @@ where warn!( client_id = self.context.client_id, msg = "Unmappable statement", - mapping_errors_enabled = self.context.mapping_errors_enabled(), error = err.to_string(), ); counter!(STATEMENTS_UNMAPPABLE_TOTAL).increment(1); @@ -1191,6 +1189,40 @@ where } } + /// + /// Decides what to do with a statement that could not be type checked. + /// + /// Returns `true` if the statement references a table carrying at least one encrypted column, + /// in which case the type check failure must be fatal. Passing such a statement through is + /// never a graceful degradation: an unmapped read hands the client raw ciphertext, an unmapped + /// predicate compares a plaintext literal against a jsonb payload, and an unmapped write stores + /// the value unencrypted. + /// + /// Returns `false` when the statement touches no encrypted column, in which case it is + /// forwarded unmodified as before. This is not a loophole left open for convenience — it is + /// load bearing. `requires_type_check` is purely syntactic, so *every* query, insert, update, + /// delete, merge, prepare and explain is type checked whether or not encryption is involved, + /// and the mapper's SQL coverage is narrower than PostgreSQL's. Statements that legitimately + /// fail here today include `pg_catalog` introspection (`Table not found: pg_catalog.pg_type`), + /// which essentially every PostgreSQL driver issues on connect, and plaintext-only queries + /// using constructs the type system cannot model (`ARRAY_AGG`/`CARDINALITY`, for instance). + /// Rejecting those would break working applications for no security benefit, since there is no + /// encrypted data anywhere in the statement to get wrong. + /// + fn statement_may_touch_eql_columns(&self, statement: &ast::Statement) -> bool { + let may_touch = + eql_mapper::may_touch_eql_columns(self.context.get_table_resolver(), statement); + + if !may_touch { + debug!(target: MAPPER, + client_id = self.context.client_id, + msg = "Unmappable statement references no encrypted columns, passing through", + ); + } + + may_touch + } + /// /// Send an ReadyForQuery to the client and remove error state. /// diff --git a/packages/eql-mapper/src/eql_mapper.rs b/packages/eql-mapper/src/eql_mapper.rs index 2df75aec1..90882a608 100644 --- a/packages/eql-mapper/src/eql_mapper.rs +++ b/packages/eql-mapper/src/eql_mapper.rs @@ -2,13 +2,14 @@ use super::importer::{ImportError, Importer}; use crate::{ inference::{TypeError, TypeInferencer}, unifier::{EqlTerm, Projection, Type, Unifier, Value}, - DepMut, Param, ParamError, ScopeError, ScopeTracker, TableResolver, TypeCheckedStatement, - TypeRegistry, + ColumnKind, DepMut, Param, ParamError, ScopeError, ScopeTracker, TableResolver, + TypeCheckedStatement, TypeRegistry, }; use sqltk::parser::ast::{self as ast, Statement}; use sqltk::{Break, NodeKey, Visitable, Visitor}; use std::{ - cell::RefCell, collections::HashMap, marker::PhantomData, ops::ControlFlow, rc::Rc, sync::Arc, + cell::RefCell, collections::HashMap, convert::Infallible, marker::PhantomData, + ops::ControlFlow, rc::Rc, sync::Arc, }; use tracing::{event, span, Level}; @@ -70,6 +71,66 @@ pub fn requires_type_check(statement: &Statement) -> bool { ) } +/// Returns whether a [`Statement`] could possibly read or write an encrypted column. +/// +/// This exists to answer one question, and only that question: when [`type_check`] has *failed*, is +/// it safe to send the statement to the database unmodified? +/// +/// A failed type check leaves no typing information behind, so the statement's every [`ObjectName`] +/// is resolved against the schema instead. That over-collects — a function name is an `ObjectName` +/// too — but over-collecting is the harmless direction: an unresolvable name is simply not a table +/// with encrypted columns. +/// +/// The answer is deliberately asymmetric: +/// +/// - A name that resolves to a table carrying at least one EQL column => `true`. The statement is +/// unsafe to pass through, because an unmapped read returns raw ciphertext and an unmapped write +/// stores plaintext. +/// - A name that does not resolve => *not* evidence of encryption. The schema has never heard of +/// it, so it has no encrypted columns to expose. `pg_catalog` introspection, which every +/// PostgreSQL driver issues and which the mapper cannot type, lands here. +/// - No name resolves to an encrypted table => `false`. The statement touches only native columns +/// and passing it through is exactly as safe as it was before Proxy sat in the path. +/// +/// Because every table reference in a statement *is* an `ObjectName`, this cannot miss a table. It +/// can only be defeated by a reference that hides encrypted columns behind a name the schema +/// records as native — a schema-loading concern, not one this function can address. +pub fn may_touch_eql_columns(resolver: Arc, statement: &Statement) -> bool { + struct EncryptedTableFinder { + resolver: Arc, + found: bool, + } + + impl<'ast> Visitor<'ast> for EncryptedTableFinder { + type Error = Infallible; + + fn enter(&mut self, node: &'ast N) -> ControlFlow> { + if let Some(name) = node.downcast_ref::() { + if let Ok(table) = self.resolver.resolve_table(name) { + if table + .columns + .iter() + .any(|col| matches!(col.kind, ColumnKind::Eql(_, _))) + { + self.found = true; + return ControlFlow::Break(Break::Finished); + } + } + } + ControlFlow::Continue(()) + } + } + + let mut visitor = EncryptedTableFinder { + resolver, + found: false, + }; + + let _ = statement.accept(&mut visitor); + + visitor.found +} + /// The error type returned by various functions in the `eql_mapper` crate. #[derive(Debug, PartialEq, Eq, thiserror::Error)] pub enum EqlMapperError { diff --git a/packages/eql-mapper/src/lib.rs b/packages/eql-mapper/src/lib.rs index 72d40cd17..48ad04020 100644 --- a/packages/eql-mapper/src/lib.rs +++ b/packages/eql-mapper/src/lib.rs @@ -40,7 +40,7 @@ pub(crate) use transformation_rules::*; #[cfg(test)] mod test { - use super::{test_helpers::*, type_check}; + use super::{may_touch_eql_columns, test_helpers::*, type_check}; use crate::{ projection, schema, test_helpers, unifier::{ @@ -3739,4 +3739,134 @@ mod test { type_check(schema, &statement).unwrap(); } + + /// A schema with one encrypted table and one that is entirely native. + fn mixed_schema() -> Arc { + resolver(schema! { + tables: { + patients: { + id, + name, + age (EQL: Ord), + } + plaintext: { + id, + note, + } + } + }) + } + + /// `may_touch_eql_columns` decides whether a statement that failed to type check is safe to + /// forward to the database unmodified. Anything touching an encrypted table must not be. + #[test] + fn may_touch_eql_columns_detects_an_encrypted_table() { + assert!(may_touch_eql_columns( + mixed_schema(), + &parse("SELECT age FROM patients") + )); + } + + #[test] + fn may_touch_eql_columns_detects_an_encrypted_table_on_the_write_path() { + assert!(may_touch_eql_columns( + mixed_schema(), + &parse("INSERT INTO patients (id, age) VALUES (1, 42)") + )); + + assert!(may_touch_eql_columns( + mixed_schema(), + &parse("UPDATE patients SET age = 42 WHERE id = 1") + )); + + assert!(may_touch_eql_columns( + mixed_schema(), + &parse("DELETE FROM patients WHERE age = 42") + )); + } + + /// The encrypted table is only named in the source of the insert, never the target. It still + /// has to be seen: an unmapped read of `patients.age` would copy raw ciphertext into a native + /// column. + #[test] + fn may_touch_eql_columns_detects_an_encrypted_table_nested_in_a_statement() { + assert!(may_touch_eql_columns( + mixed_schema(), + &parse("INSERT INTO plaintext (id, note) SELECT id, age FROM patients") + )); + + assert!(may_touch_eql_columns( + mixed_schema(), + &parse("SELECT id FROM plaintext WHERE id IN (SELECT id FROM patients)") + )); + + assert!(may_touch_eql_columns( + mixed_schema(), + &parse("WITH p AS (SELECT age FROM patients) SELECT * FROM p") + )); + } + + /// A statement over native columns only carries no encrypted data to get wrong, so a type + /// check failure on it is not a correctness problem and it is forwarded as before. + #[test] + fn may_touch_eql_columns_ignores_a_native_only_statement() { + assert!(!may_touch_eql_columns( + mixed_schema(), + &parse("SELECT note FROM plaintext WHERE id = 1") + )); + + assert!(!may_touch_eql_columns( + mixed_schema(), + &parse("INSERT INTO plaintext (id, note) VALUES (1, 'hello')") + )); + } + + /// The case that makes this check load bearing rather than a nicety: driver introspection of + /// `pg_catalog` cannot be type checked, and every PostgreSQL driver issues it. + #[test] + fn may_touch_eql_columns_ignores_tables_absent_from_the_schema() { + let statement = parse( + "SELECT attname, atttypid FROM pg_catalog.pg_attribute WHERE attnum > 0", + ); + + // Precondition: this statement genuinely cannot be type checked. + assert!(type_check(mixed_schema(), &statement).is_err()); + + assert!(!may_touch_eql_columns(mixed_schema(), &statement)); + } + + /// A table the schema has never heard of has no encrypted columns to expose. PostgreSQL + /// rejects the statement itself with `relation "..." does not exist`. + #[test] + fn may_touch_eql_columns_ignores_an_unknown_table() { + assert!(!may_touch_eql_columns( + mixed_schema(), + &parse("SELECT * FROM no_such_table") + )); + } + + /// A statement referencing no table at all cannot touch an encrypted column. + #[test] + fn may_touch_eql_columns_ignores_a_tableless_statement() { + assert!(!may_touch_eql_columns(mixed_schema(), &parse("SELECT 1"))); + } + + /// A schema with no encrypted columns anywhere must never make a type check failure fatal — + /// Proxy in front of an unencrypted database should stay out of the way. + #[test] + fn may_touch_eql_columns_is_false_for_a_schema_with_no_encrypted_columns() { + let schema = resolver(schema! { + tables: { + plaintext: { + id, + note, + } + } + }); + + assert!(!may_touch_eql_columns( + schema, + &parse("SELECT note FROM plaintext") + )); + } } diff --git a/tests/mise.tcp.toml b/tests/mise.tcp.toml index a017e33a7..d1a5fe8c1 100644 --- a/tests/mise.tcp.toml +++ b/tests/mise.tcp.toml @@ -3,5 +3,4 @@ CS_DATABASE__HOST = "postgres" CS_DATABASE__PORT = "5532" CS_PROMETHEUS__ENABLED = "true" CS_LOG__LEVEL = "debug" -CS_DEVELOPMENT__ENABLE_MAPPING_ERRORS = "true" CS_DEVELOPMENT__DISABLE_MAPPING = "false" diff --git a/tests/mise.tls.toml b/tests/mise.tls.toml index 7de9349bd..02fee6923 100644 --- a/tests/mise.tls.toml +++ b/tests/mise.tls.toml @@ -6,4 +6,3 @@ CS_TLS__TYPE = "Path" CS_TLS__CERTIFICATE_PATH = "/etc/cipherstash-proxy/server.cert" CS_TLS__PRIVATE_KEY_PATH = "/etc/cipherstash-proxy/server.key" CS_SERVER__REQUIRE_TLS = "true" -CS_DEVELOPMENT__ENABLE_MAPPING_ERRORS = "true" From 554ff05bda3444539b4576fa901c178c7c10095b Mon Sep 17 00:00:00 2001 From: James Sadler Date: Thu, 30 Jul 2026 00:14:25 +1000 Subject: [PATCH 3/4] fix(mapper): refuse an unmappable statement that touches an encrypted column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `CS_DEVELOPMENT__ENABLE_MAPPING_ERRORS` defaulted to false, so when Proxy could not type check a statement it logged a warning and sent the statement to PostgreSQL unmapped. That is not a degraded answer, it is a wrong one, and none of the ways it goes wrong announce themselves: an unmapped SELECT returns the raw EQL payload instead of the decrypted value, an unmapped WHERE compares a plaintext literal against a jsonb payload so it matches nothing or the wrong rows, and an unmapped INSERT/UPDATE writes the value to disk unencrypted. The ticket's reproducer, `SELECT DISTINCT encrypted_bool FROM encrypted`, returned rows of ciphertext to the client with no error at all — `eql_v3_boolean` is storage-only and carries no equality term, so DISTINCT cannot be keyed on it. The flag is gone and there is no way to restore the old behaviour. Not fatal outright, though, which is the part worth explaining. The ticket flagged the risk that unmappable statements are not all encryption-related, and they are not: `requires_type_check` is purely syntactic, so every query is type checked whether or not encryption is involved, and the mapper's SQL coverage is narrower than PostgreSQL's. Measuring it settled the question. Running the existing suite against a Proxy with the flag forced on — which is exactly unconditional fatality — fails 253 of 349 tests, because tokio-postgres queries `pg_catalog.pg_type` to resolve the OID of an EQL v3 domain, so under EQL v3 an ordinary client issues an untypeable statement on the way to nearly every encrypted one. psql fares no better: `\d`, `\dt`, `\l`, `\dn` and `\df` all fail with `Table not found: pg_catalog.pg_*`. Rejecting any of that buys no security, because none of those statements contain encrypted data to get wrong. So the check is narrowed rather than removed. `may_touch_eql_columns` resolves every ObjectName in the statement against the schema and asks whether any names a table with an EQL column. A name that does not resolve is not evidence of encryption — the schema has never heard of it, so it has nothing to expose — which is what lets pg_catalog through. Over-collecting is the safe direction and the only direction this can err in: a function name is an ObjectName too. Qualified names are matched on their last identifier rather than passed to `resolve_table` whole. `resolve_table` only accepts a bare name and answers TableNotFound for `public.encrypted`, so taking it at face value would have waved through the exact shape the check exists to catch. STATEMENTS_UNMAPPABLE_TOTAL still counts every statement that failed to type check, so the metric stays comparable across the change; the ones now forwarded also increment STATEMENTS_PASSTHROUGH_TOTAL, like every other passthrough. Two of the new integration tests duplicated existing coverage and are dropped: `passthrough_invalid_statement` already pins that an unknown table is reported with PostgreSQL's own error, and `passthrough_select_with_cardinality` already pins that an untypeable plaintext-only query still runs. `unmappable_table_not_found` absorbs the first: it asserted only `is_err()`, and now asserts the error text, which is what actually distinguishes forwarding from refusing. --- CHANGELOG.md | 10 ++ docs/errors.md | 26 ++-- .../src/select/unmappable.rs | 118 ++++-------------- .../src/postgresql/frontend.rs | 24 +++- packages/eql-mapper/src/eql_mapper.rs | 52 +++++--- packages/eql-mapper/src/lib.rs | 31 +++++ 6 files changed, 131 insertions(+), 130 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ffc99897..bb9d80828 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Removed + +- **`CS_DEVELOPMENT__ENABLE_MAPPING_ERRORS` (breaking behavioural change)**: this setting defaulted to off, which meant that when Proxy could not map a statement it logged a warning and sent the statement to PostgreSQL unmapped. That is not a degraded answer, it is a wrong one: an unmapped `SELECT` returned the raw EQL payload instead of the decrypted value, an unmapped `WHERE` compared a plaintext literal against a jsonb payload and so matched nothing or the wrong rows, and an unmapped `INSERT`/`UPDATE` wrote the value to disk without encrypting it — none of which reported an error. + + A statement that fails to type check **and references a table with encrypted columns** is now always an error. The setting is gone; there is no way to restore the old behaviour. If you were relying on passthrough, the statement was almost certainly not doing what you thought. + + A statement that fails to type check and references **no** encrypted column is still forwarded unchanged, exactly as before. This is what keeps `pg_catalog` introspection working — `psql`'s `\d`, `\dt` and `\l`, and the type lookups client libraries issue to resolve an EQL domain OID, cannot be type checked and never could be. + + The `cipherstash_proxy_statements_unmappable_total` metric is unchanged and still counts every statement that failed to type check, so it remains comparable across the upgrade. + ### Changed - **EQL v3 (searchable encryption)**: Proxy now targets EQL v3. Encrypted columns are declared with self-configuring, typed `jsonb` domains (for example `eql_v3_text_search`, `eql_v3_integer_ord`, `eql_v3_json_search`) that encode both the scalar type and the column's searchable capabilities in the column type itself, replacing EQL v2's opaque `eql_v2_encrypted` composite type and its separate `eql_v2_configuration` table. The bundled `cipherstash-client` is upgraded to 0.42.0 and EQL to 3.0.4. Existing v2-encrypted data and schemas must be migrated to v3. diff --git a/docs/errors.md b/docs/errors.md index 6d5035acf..c1f53e652 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -217,31 +217,27 @@ Statement could not be type checked: '{type-check-error-message}' CipherStash Proxy checks SQL statements against the database schema to transparently encrypt and decrypt data. -The behaviour of Proxy depends on the `mapping_errors_enabled` configuration. +What Proxy does when that check fails depends on whether the statement references a table with encrypted columns. -When `mapping_errors_enabled` is `false` (the default), then type check errors are logged, and the statement is passed through to the database. +**If it does, the statement is refused.** SQL is large and complex and the mapper's coverage is narrower than PostgreSQL's, so a type check failure is not always a real defect in your statement — but sending an unmapped statement to the database is not a lesser version of the right answer, it is a wrong one: -When `mapping_errors_enabled` is `true`, then type check errors are raised, and statement execution halts. +- a `SELECT` returns the raw EQL payload (`{"c": "mBbK [!NOTE] +> This behaviour used to be governed by a `CS_DEVELOPMENT__ENABLE_MAPPING_ERRORS` setting that defaulted to off, which meant unmappable statements touching encrypted columns were silently forwarded. The setting has been removed. See the [changelog](../CHANGELOG.md). ### How to fix In most cases, this error will occur if the statement contains invalid or unsupported syntax. +The error message names the specific problem. A common one is asking for an operation the column's EQL domain does not support — ordering a column whose domain carries no ordering term, for instance, or `SELECT DISTINCT` on a storage-only domain such as `eql_v3_boolean`, which carries no equality term. In that case either use a domain with the capability you need, or drop the operation. + Check if you are running the latest version of CipherStash Proxy, and update to the latest version if not. If the error persists, please contact CipherStash [support](https://cipherstash.com/support). diff --git a/packages/cipherstash-proxy-integration/src/select/unmappable.rs b/packages/cipherstash-proxy-integration/src/select/unmappable.rs index 12c954065..6e7a3ce30 100644 --- a/packages/cipherstash-proxy-integration/src/select/unmappable.rs +++ b/packages/cipherstash-proxy-integration/src/select/unmappable.rs @@ -4,11 +4,15 @@ mod tests { use std::error::Error; /// - /// Tests that a statement Proxy cannot map is refused. + /// A statement Proxy cannot map is refused when it touches an encrypted column. There is no + /// configuration that turns this off: forwarding such a statement does not degrade the answer, + /// it makes it wrong — an unmapped read returns raw ciphertext, an unmapped predicate compares + /// a plaintext literal against a jsonb payload, and an unmapped write stores plaintext. /// - /// There is no configuration that turns this off. A statement that fails to type check and - /// touches an encrypted column is always an error, because forwarding it produces a wrong - /// answer rather than a degraded one. + /// `vtha` is not in the schema, so it has no encrypted columns to expose and the statement is + /// forwarded. The error the client sees is PostgreSQL's own, not one Proxy invented, and that + /// matters: clients rely on `relation "..." does not exist` to distinguish a missing table from + /// a proxy fault. /// #[tokio::test] async fn unmappable_table_not_found() { @@ -17,10 +21,13 @@ mod tests { let sql = "SELECT blah FROM vtha"; let result = client.query(sql, &[]).await; - assert!( - result.is_err(), - "Expected unmappble SQL statement to return an error", - ); + match result { + Ok(_) => panic!("Expected an error for an unknown table"), + Err(error) => { + let db_error = error.source().unwrap().to_string(); + assert_eq!(db_error, "ERROR: relation \"vtha\" does not exist"); + } + } } #[tokio::test] @@ -62,6 +69,8 @@ mod tests { ); } + /// + /// The reproducer for CIP-3680. /// /// `eql_v3_boolean` is storage-only: it carries no equality term, so `DISTINCT` cannot be keyed /// on it and the statement fails to type check. @@ -80,10 +89,6 @@ mod tests { let sql = "INSERT INTO encrypted (id, encrypted_bool) VALUES ($1, $2)"; client.query(sql, &[&id, &true]).await.unwrap(); - // A fresh connection, so the assertion is about the refusal itself and not about how the - // driver frames an error arriving on a connection it has already used. - let client = connect_with_tls(*PROXY).await; - let sql = "SELECT DISTINCT encrypted_bool FROM encrypted"; let result = client.query(sql, &[]).await; @@ -154,11 +159,16 @@ mod tests { /// The counterpart, and the reason the unmappable check is narrowed to statements touching an /// encrypted column rather than made fatal outright. /// - /// `requires_type_check` is purely syntactic, so every `SELECT` is type checked whether or not - /// encryption is involved, and the mapper's SQL coverage is narrower than PostgreSQL's. Driver - /// introspection of `pg_catalog` fails to type check (`Table not found: pg_catalog.pg_type`) - /// and essentially every PostgreSQL driver issues it. Rejecting it would break working - /// applications for no security benefit — there is no encrypted data in the statement. + /// `requires_type_check` is purely syntactic, so every query is type checked whether or not + /// encryption is involved, and the mapper's SQL coverage is narrower than PostgreSQL's. + /// Introspection of `pg_catalog` fails to type check (`Table not found: pg_catalog.pg_type`), + /// and it is not an exotic thing to issue: `psql`'s `\d`, `\dt` and `\l` are all this shape, + /// and tokio-postgres itself issues one to resolve the OID of an EQL v3 domain. Rejecting them + /// would break working applications for no security benefit — there is no encrypted data in + /// the statement to get wrong. + /// + /// `passthrough::tests::passthrough_select_with_cardinality` covers the same rule for an + /// ordinary query over a table with no encrypted columns. /// #[tokio::test] async fn unmappable_statement_with_no_encrypted_columns_is_forwarded() { @@ -172,78 +182,4 @@ mod tests { "Expected pg_catalog introspection to still be forwarded to the database", ); } - - /// - /// The same rule applied to an ordinary query over a table with no encrypted columns: - /// `ARRAY_AGG`/`CARDINALITY` cannot be typed by the mapper, but the statement is harmless. - /// - #[tokio::test] - async fn unmappable_native_only_statement_is_forwarded() { - clear().await; - - let client = connect_with_tls(*PROXY).await; - - let id = random_id(); - let sql = "INSERT INTO plaintext (id, plaintext) VALUES ($1, $2)"; - client - .query(sql, &[&id, &"hello@cipherstash.com"]) - .await - .unwrap(); - - let sql = "SELECT ARRAY_REMOVE(ARRAY_AGG(id), NULL), plaintext - FROM plaintext - WHERE CARDINALITY(ARRAY[1,2]) <> 0 - GROUP BY plaintext"; - let rows = client.query(sql, &[]).await.unwrap(); - - assert_eq!(rows.len(), 1); - } - - /// - /// A statement over a table the schema has never heard of has no encrypted columns to expose, - /// so it is forwarded and PostgreSQL rejects it with its own error rather than Proxy inventing - /// one. Clients depend on seeing the real database error. - /// - #[tokio::test] - async fn unknown_table_is_reported_by_postgres_not_proxy() { - let client = connect_with_tls(*PROXY).await; - - let sql = "SELECT * FROM blahvtha"; - let result = client.query(sql, &[]).await; - - match result { - Ok(_) => panic!("Expected an error for an unknown table"), - Err(error) => { - let db_error = error.source().unwrap().to_string(); - assert_eq!(db_error, "ERROR: relation \"blahvtha\" does not exist"); - } - } - } - - /// - /// A read that Proxy cannot map must not fall back to handing the client raw EQL payloads. - /// - #[tokio::test] - async fn unmappable_read_does_not_leak_ciphertext_to_the_client() { - clear().await; - - let client = connect_with_tls(*PROXY).await; - - let id = random_id(); - let sql = "INSERT INTO encrypted (id, encrypted_text) VALUES ($1, $2)"; - client - .query(sql, &[&id, &"hello@cipherstash.com"]) - .await - .unwrap(); - - // Native and encrypted cannot be unified, so this cannot be mapped. - let sql = "SELECT encrypted_text FROM encrypted WHERE plaintext = encrypted_text"; - let result = client.query(sql, &[]).await; - - assert!( - result.is_err(), - "Expected an unmappable read of an encrypted column to be refused rather than \ - returning raw EQL payloads", - ); - } } diff --git a/packages/cipherstash-proxy/src/postgresql/frontend.rs b/packages/cipherstash-proxy/src/postgresql/frontend.rs index 9e8ae3c04..2ddd08f8f 100644 --- a/packages/cipherstash-proxy/src/postgresql/frontend.rs +++ b/packages/cipherstash-proxy/src/postgresql/frontend.rs @@ -1202,12 +1202,24 @@ where /// forwarded unmodified as before. This is not a loophole left open for convenience — it is /// load bearing. `requires_type_check` is purely syntactic, so *every* query, insert, update, /// delete, merge, prepare and explain is type checked whether or not encryption is involved, - /// and the mapper's SQL coverage is narrower than PostgreSQL's. Statements that legitimately - /// fail here today include `pg_catalog` introspection (`Table not found: pg_catalog.pg_type`), - /// which essentially every PostgreSQL driver issues on connect, and plaintext-only queries - /// using constructs the type system cannot model (`ARRAY_AGG`/`CARDINALITY`, for instance). - /// Rejecting those would break working applications for no security benefit, since there is no - /// encrypted data anywhere in the statement to get wrong. + /// and the mapper's SQL coverage is narrower than PostgreSQL's. + /// + /// Making the failure fatal unconditionally was measured before it was rejected, and it is not + /// survivable. With the old `enable_mapping_errors` flag forced on — which is exactly that + /// behaviour — `psql` loses every catalogue metacommand: + /// + /// ```text + /// \d => ERROR: Statement could not be type checked: Table not found: pg_catalog.pg_class + /// \l => ERROR: Statement could not be type checked: Table not found: pg_catalog.pg_database + /// \dn => ERROR: Statement could not be type checked: Table not found: pg_catalog.pg_namespace + /// ``` + /// + /// and 253 of the 349 integration tests fail, because tokio-postgres queries + /// `pg_catalog.pg_type` to resolve the OID of an EQL v3 domain — so under EQL v3 an ordinary + /// client issues one of these on the way to almost every encrypted statement. Plaintext-only + /// queries using constructs the type system cannot model (`ARRAY_AGG`/`CARDINALITY`) fail here + /// too. Rejecting any of it would break working applications for no security benefit, since + /// there is no encrypted data anywhere in those statements to get wrong. /// fn statement_may_touch_eql_columns(&self, statement: &ast::Statement) -> bool { let may_touch = diff --git a/packages/eql-mapper/src/eql_mapper.rs b/packages/eql-mapper/src/eql_mapper.rs index 90882a608..b915df046 100644 --- a/packages/eql-mapper/src/eql_mapper.rs +++ b/packages/eql-mapper/src/eql_mapper.rs @@ -76,45 +76,61 @@ pub fn requires_type_check(statement: &Statement) -> bool { /// This exists to answer one question, and only that question: when [`type_check`] has *failed*, is /// it safe to send the statement to the database unmodified? /// -/// A failed type check leaves no typing information behind, so the statement's every [`ObjectName`] -/// is resolved against the schema instead. That over-collects — a function name is an `ObjectName` -/// too — but over-collecting is the harmless direction: an unresolvable name is simply not a table -/// with encrypted columns. +/// A failed type check leaves no typing information behind, so the statement's every +/// [`ast::ObjectName`] is resolved against the schema instead. That over-collects — a function name +/// is an `ObjectName` too — but over-collecting is the harmless direction: it can only make a +/// statement fatal that would have been safe to forward, never the reverse. /// /// The answer is deliberately asymmetric: /// /// - A name that resolves to a table carrying at least one EQL column => `true`. The statement is -/// unsafe to pass through, because an unmapped read returns raw ciphertext and an unmapped write -/// stores plaintext. +/// unsafe to pass through, because an unmapped read returns raw ciphertext, an unmapped predicate +/// compares a plaintext literal against a jsonb payload, and an unmapped write stores plaintext. /// - A name that does not resolve => *not* evidence of encryption. The schema has never heard of /// it, so it has no encrypted columns to expose. `pg_catalog` introspection, which every /// PostgreSQL driver issues and which the mapper cannot type, lands here. /// - No name resolves to an encrypted table => `false`. The statement touches only native columns /// and passing it through is exactly as safe as it was before Proxy sat in the path. /// -/// Because every table reference in a statement *is* an `ObjectName`, this cannot miss a table. It -/// can only be defeated by a reference that hides encrypted columns behind a name the schema -/// records as native — a schema-loading concern, not one this function can address. +/// Qualified names are matched on their final identifier, not handed to +/// [`TableResolver::resolve_table`] whole. `resolve_table` only accepts a bare name — the schema +/// model has no notion of a namespace — so it answers `TableNotFound` for `public.encrypted`, and +/// taking that at face value would report the one shape this function exists to catch as safe. The +/// cost is that `anything.encrypted` is treated as the encrypted `encrypted`, which is the +/// conservative direction and matches how the rest of the mapper already reads such a name. pub fn may_touch_eql_columns(resolver: Arc, statement: &Statement) -> bool { struct EncryptedTableFinder { resolver: Arc, found: bool, } + impl EncryptedTableFinder { + fn is_encrypted_table(&self, name: &ast::ObjectName) -> bool { + let Some(ast::ObjectNamePart::Identifier(ident)) = name.0.last() else { + return false; + }; + + let bare = ast::ObjectName(vec![ast::ObjectNamePart::Identifier(ident.clone())]); + + self.resolver + .resolve_table(&bare) + .is_ok_and(|table| { + table + .columns + .iter() + .any(|col| matches!(col.kind, ColumnKind::Eql(_, _))) + }) + } + } + impl<'ast> Visitor<'ast> for EncryptedTableFinder { type Error = Infallible; fn enter(&mut self, node: &'ast N) -> ControlFlow> { if let Some(name) = node.downcast_ref::() { - if let Ok(table) = self.resolver.resolve_table(name) { - if table - .columns - .iter() - .any(|col| matches!(col.kind, ColumnKind::Eql(_, _))) - { - self.found = true; - return ControlFlow::Break(Break::Finished); - } + if self.is_encrypted_table(name) { + self.found = true; + return ControlFlow::Break(Break::Finished); } } ControlFlow::Continue(()) diff --git a/packages/eql-mapper/src/lib.rs b/packages/eql-mapper/src/lib.rs index 48ad04020..0373a9166 100644 --- a/packages/eql-mapper/src/lib.rs +++ b/packages/eql-mapper/src/lib.rs @@ -3851,6 +3851,37 @@ mod test { assert!(!may_touch_eql_columns(mixed_schema(), &parse("SELECT 1"))); } + /// `Schema::resolve_table` only accepts a bare name and answers `TableNotFound` for anything + /// qualified, so a check written in terms of it would wave `public.patients` straight through + /// — the exact shape it exists to stop. Qualified names are matched on their last identifier. + #[test] + fn may_touch_eql_columns_detects_a_schema_qualified_encrypted_table() { + let statement = parse("SELECT age FROM public.patients"); + + // Precondition: the resolver really cannot resolve the qualified name, so this test is + // exercising the fallback and not passing for some other reason. + assert!(mixed_schema() + .resolve_table(&ast::ObjectName(vec![ + ast::ObjectNamePart::Identifier(Ident::new("public")), + ast::ObjectNamePart::Identifier(Ident::new("patients")), + ])) + .is_err()); + + assert!(may_touch_eql_columns(mixed_schema(), &statement)); + } + + /// The price of matching on the last identifier: a qualified name whose final part collides + /// with an encrypted table is treated as that table. Over-strict, and deliberately so — it + /// costs an error on a statement that could have been forwarded, where being wrong the other + /// way costs plaintext on disk. + #[test] + fn may_touch_eql_columns_treats_a_colliding_qualified_name_as_encrypted() { + assert!(may_touch_eql_columns( + mixed_schema(), + &parse("SELECT age FROM some_other_schema.patients") + )); + } + /// A schema with no encrypted columns anywhere must never make a type check failure fatal — /// Proxy in front of an unencrypted database should stay out of the way. #[test] From a0cec5176fbab7dd772841f9ed4b5208e729e79a Mon Sep 17 00:00:00 2001 From: James Sadler Date: Thu, 30 Jul 2026 00:32:17 +1000 Subject: [PATCH 4/4] test(integration): pin the two behaviours the refusal actually has MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two consequences of making an unmappable statement fatal, both found by running the suite rather than by reading the diff. `encrypted_column_not_defined_in_schema` asserted PostgreSQL's `column "..." does not exist`, which it only ever saw because the statement was forwarded. It names `encrypted`, so it is now refused by Proxy and the error is a mapping error. Worth being explicit about why this one is not carved out: a column that does not exist cannot leak, so forwarding looks safe — but that only holds if Proxy's schema is current, and it is reloaded on an interval. Between an ALTER TABLE adding an encrypted column and the next reload, "Proxy has not heard of this column" and "this column does not exist" are different claims, and acting on the first would write plaintext into the new column. The message is worse; the alternative is occasionally wrong. The second is not mine and is not fixed here, but it has to be recorded because removing the flag is what exposes it. On a connection that has already run a statement — every connection in a pool — a refusal reaches the client as tokio-postgres' `unexpected message from server` rather than the mapping error, and the connection is left broken. Frontend and backend are separate tasks writing to one unbounded channel, so an ErrorResponse synthesised on the frontend is queued at once and overtakes responses the server still owes for earlier messages. For `Close(s0) Sync | Parse(s1) Describe(s1) Sync` the client receives ErrorResponse, ReadyForQuery, CloseComplete, ReadyForQuery when it is waiting for CloseComplete first. The flag previously kept that path from being reached by default, so the ticket's premise — that Proxy already produces a good error in every one of these cases — holds only for a connection's first statement. The predecessor on this branch read the same failure and put it down to a connection-reuse artifact of the test; it is not, it is a Proxy defect, and probing it shows a fresh connection reports the error correctly while any reused one does not. So there are now two tests. The fresh-connection one keeps the strong assertion on the message. The reused-connection one asserts only that the statement is refused, which is the security property, and carries the byte order in its doc comment so the weak assertion is not mistaken for the intended behaviour. Fixing the ordering needs the frontend to withhold a synthetic error until the backend has drained, which is a change to the proxy's concurrency model. --- .../src/extended_protocol_error_messages.rs | 22 ++++++- .../src/select/unmappable.rs | 58 ++++++++++++++++++- packages/eql-mapper/src/eql_mapper.rs | 14 ++--- packages/eql-mapper/src/lib.rs | 5 +- 4 files changed, 84 insertions(+), 15 deletions(-) diff --git a/packages/cipherstash-proxy-integration/src/extended_protocol_error_messages.rs b/packages/cipherstash-proxy-integration/src/extended_protocol_error_messages.rs index dbeccae4d..b2d3072ec 100644 --- a/packages/cipherstash-proxy-integration/src/extended_protocol_error_messages.rs +++ b/packages/cipherstash-proxy-integration/src/extended_protocol_error_messages.rs @@ -16,6 +16,26 @@ mod tests { } } + /// Naming a column that is not in the schema is rejected by Proxy, not forwarded for + /// PostgreSQL to reject. + /// + /// This used to assert PostgreSQL's `column "..." of relation "..." does not exist`, because + /// the statement failed to type check and Proxy forwarded whatever it could not map. It no + /// longer does when the statement names a table with encrypted columns, and `encrypted` is + /// such a table. + /// + /// It is tempting to argue this particular shape was safe to forward — a column that does not + /// exist can hardly leak, since PostgreSQL will reject the statement anyway. That reasoning + /// only holds if Proxy's view of the schema is current, and it is not guaranteed to be: the + /// schema is reloaded on an interval, so between a `ALTER TABLE ... ADD COLUMN` of an encrypted + /// column and the next reload, "Proxy has never heard of this column" and "this column does not + /// exist" are different statements. Trusting the first would write plaintext into the new + /// column. The cost of not trusting it is this less specific error message. + /// + /// The message reads oddly — the table and column names are the wrong way round in the + /// mapper's `SchemaError::ColumnNotFound` formatting. That is a pre-existing defect in the + /// error text, unrelated to which side of the fence the statement lands on; it is pinned here + /// rather than quietly corrected so that fixing it is a deliberate change. #[tokio::test] async fn encrypted_column_not_defined_in_schema() { trace(); @@ -37,7 +57,7 @@ mod tests { if let Err(err) = result { let msg = err.to_string(); - assert_eq!(msg, "db error: ERROR: column \"encrypted_unconfigured\" of relation \"encrypted\" does not exist"); + assert_eq!(msg, "db error: ERROR: Statement could not be type checked: Column: encrypted not found for table: encrypted_unconfigured. For help visit https://github.com/cipherstash/proxy/blob/main/docs/errors.md#mapping-statement-could-not-be-type-checked"); } else { unreachable!(); } diff --git a/packages/cipherstash-proxy-integration/src/select/unmappable.rs b/packages/cipherstash-proxy-integration/src/select/unmappable.rs index 6e7a3ce30..b34404e22 100644 --- a/packages/cipherstash-proxy-integration/src/select/unmappable.rs +++ b/packages/cipherstash-proxy-integration/src/select/unmappable.rs @@ -79,15 +79,19 @@ mod tests { /// payloads — `{"c": "mBbK, statement: &Statement let bare = ast::ObjectName(vec![ast::ObjectNamePart::Identifier(ident.clone())]); - self.resolver - .resolve_table(&bare) - .is_ok_and(|table| { - table - .columns - .iter() - .any(|col| matches!(col.kind, ColumnKind::Eql(_, _))) - }) + self.resolver.resolve_table(&bare).is_ok_and(|table| { + table + .columns + .iter() + .any(|col| matches!(col.kind, ColumnKind::Eql(_, _))) + }) } } diff --git a/packages/eql-mapper/src/lib.rs b/packages/eql-mapper/src/lib.rs index 0373a9166..9e47fda63 100644 --- a/packages/eql-mapper/src/lib.rs +++ b/packages/eql-mapper/src/lib.rs @@ -3825,9 +3825,8 @@ mod test { /// `pg_catalog` cannot be type checked, and every PostgreSQL driver issues it. #[test] fn may_touch_eql_columns_ignores_tables_absent_from_the_schema() { - let statement = parse( - "SELECT attname, atttypid FROM pg_catalog.pg_attribute WHERE attnum > 0", - ); + let statement = + parse("SELECT attname, atttypid FROM pg_catalog.pg_attribute WHERE attnum > 0"); // Precondition: this statement genuinely cannot be type checked. assert!(type_check(mixed_schema(), &statement).is_err());