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/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..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(); @@ -26,7 +46,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"; @@ -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!(); } @@ -62,7 +82,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 +109,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 +134,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..b34404e22 100644 --- a/packages/cipherstash-proxy-integration/src/select/unmappable.rs +++ b/packages/cipherstash-proxy-integration/src/select/unmappable.rs @@ -1,30 +1,38 @@ #[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. + /// 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. /// - /// `enable_mapping_errors` should be `true` in the test configuration.` - /// - /// Test ensures that unmappable SQL statements return an error + /// `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() { - 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; - 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] 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 +45,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 +58,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; @@ -60,4 +68,170 @@ mod tests { "Expected unmappble SQL statement to return an error", ); } + + /// + /// 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. + /// + /// 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 same refusal on a connection that has already run a statement — which is every + /// connection in a pool, and so the case that actually matters in production. + /// + /// The statement is still refused, and that is the security property this file exists to pin. + /// But the client cannot read *why*: it gets tokio-postgres' `unexpected message from server` + /// instead of the mapping error, and the connection is broken rather than reusable. The + /// assertion is deliberately weak here so the test documents the defect instead of asserting + /// it away. + /// + /// The cause is an ordering bug in Proxy, not in this test, and it is older than the removal + /// of `enable_mapping_errors` — that flag simply kept the path from being reached by default. + /// Frontend and backend are separate tasks writing to one unbounded channel, so a synthetic + /// ErrorResponse raised on the frontend is queued immediately and overtakes responses the + /// server still owes for earlier messages. Observed byte order for + /// `Close(s0) Sync | Parse(s1) Describe(s1) Sync`: + /// + /// ```text + /// sent: ErrorResponse, ReadyForQuery, CloseComplete, ReadyForQuery + /// expected: CloseComplete, ReadyForQuery, ErrorResponse, ReadyForQuery + /// ``` + /// + /// The client is waiting for `CloseComplete` and gets `ErrorResponse`, so it gives up. Fixing + /// it needs the frontend to hold a synthetic error until the backend has drained what the + /// server owes, which is a change to the proxy's concurrency model and not in scope here. + /// + #[tokio::test] + async fn select_distinct_on_a_storage_only_column_is_refused_on_a_reused_connection() { + clear().await; + + let client = connect_with_tls(*PROXY).await; + + let id = random_id(); + let sql = "INSERT INTO encrypted (id, encrypted_bool) VALUES ($1, $2)"; + client.query(sql, &[&id, &true]).await.unwrap(); + + let sql = "SELECT DISTINCT encrypted_bool FROM encrypted"; + let result = client.query(sql, &[]).await; + + if let Ok(rows) = result { + panic!( + "Expected DISTINCT on a storage-only encrypted column to be refused, \ + but Proxy returned {} row(s) of raw ciphertext", + rows.len() + ); + } + } + + /// + /// 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 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() { + 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", + ); + } } 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(); 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..2ddd08f8f 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,52 @@ 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. + /// + /// 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 = + 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..78ee495f5 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,80 @@ 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 +/// [`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, 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. +/// +/// 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 self.is_encrypted_table(name) { + 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..9e47fda63 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,164 @@ 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"))); + } + + /// `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] + 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"