Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
551 changes: 308 additions & 243 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions docs/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Check the configured username and password are correct and can connect to the da

Check the database is using a supported authentication method.

CipherStash Proxy supportsseveral PostgreSQL password authentication methods:
CipherStash Proxy supports several PostgreSQL password authentication methods:

- password
- md5
Expand Down Expand Up @@ -178,7 +178,7 @@ An error occurred when attempting to type check the SQL statement.
### Error message

```
Statement could not be type checked: '{type-check-erro-message}'
Statement could not be type checked: '{type-check-error-message}'
```

### Notes
Expand Down Expand Up @@ -424,4 +424,4 @@ If using PEM-based configuration:
Check that the certificate and private key are valid.


<!-- ---------------------------------------------------------------------------------------------------- -->
<!-- ---------------------------------------------------------------------------------------------------- -->
11 changes: 11 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,17 @@ mise --env tcp run test:wait_for_postgres_to_quack --port 6432 --max-retries 20
mise --env tcp run test:integration:migrate
mise --env tcp run proxy:down

echo
echo '###############################################'
echo '# Test: Warning for missing encrypt config'
echo '###############################################'
echo

mise --env tcp run proxy:up proxy --extra-args "--detach --wait"
mise --env tcp run test:wait_for_postgres_to_quack --port 6432 --max-retries 20
mise --env tcp run test:integration:warn_missing_config
mise --env tcp run proxy:down

echo
echo '###############################################'
echo '# Test: Python'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ mod tests {
if let Err(err) = result {
let msg = err.to_string();

assert_eq!(msg, "db error: ERROR: Column 'encrypted_unconfigured' in table 'unconfigured' has no Encrypt configuration. For help visit https://github.com/cipherstash/proxy/blob/main/docs/errors.md#encrypt-unknown-column");
// This is similar to below. The error message comes from tokio-postgres when Proxy
// returns cs_encrypted_v1 and the client cannot convert to a string.
// If mapping errors are enabled (enable_mapping_errors or CS_DEVELOPMENT__ENABLE_MAPPING_ERRORS),
// then Proxy will return an error that says "Column X in table Y has no Encrypt configuration"
assert_eq!(msg, "error serializing parameter 1: cannot convert between the Rust type `&str` and the Postgres type `cs_encrypted_v1`");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately, this is not as clear now, but with mapping errors disabled (default, and how it is here) we emit a warning, and return the value to the client in cs_encrypted_v1, and this is the client (tokio-postgres) reporting an error. Not sure what else we can do to improve

Copy link
Contributor

Choose a reason for hiding this comment

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

I've run into this a couple of times with tokio-postgres and it can be a bit confusing.
Not much we can do, because is actually tokio enforcing type correctness - the statement types have not been rewritten.

} else {
unreachable!();
}
Expand Down
34 changes: 19 additions & 15 deletions packages/cipherstash-proxy/src/postgresql/frontend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ where
let sent: u64 = bytes.len() as u64;
counter!(CLIENTS_BYTES_RECEIVED_TOTAL).increment(sent);

if self.encrypt.is_passthrough() {
if self.encrypt.config.mapping_disabled() {
Copy link
Contributor Author

@yujiyokoo yujiyokoo Mar 31, 2025

Choose a reason for hiding this comment

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

is_passthrough checks if mapping is disabled or encrypt config is empty. Here it's updated to only one of them

self.write_to_server(bytes).await?;
return Ok(());
}
Expand Down Expand Up @@ -785,8 +785,7 @@ where
msg = "Configured column",
column = ?identifier
);
let col = self.get_column(identifier)?;
Some(col)
self.get_column(identifier)?
}
_ => None,
};
Expand Down Expand Up @@ -822,8 +821,7 @@ where
column = ?identifier
);

let col = self.get_column(identifier)?;
Some(col)
self.get_column(identifier)?
}
_ => None,
};
Expand All @@ -850,7 +848,9 @@ where
identifier = ?identifier
);
let col = self.get_column(identifier)?;
literal_columns.push(Some(col));
if col.is_some() {
literal_columns.push(col);
}
}
}
}
Expand All @@ -860,9 +860,9 @@ where

///
/// Get the column configuration for the Identifier
/// Returns `EncryptError::UnknownColumn` if configuratiuon cannot be found for the Identified column
/// Returns `EncryptError::UnknownColumn` if configuration cannot be found for the Identified column
///
fn get_column(&self, identifier: Identifier) -> Result<Column, Error> {
fn get_column(&self, identifier: Identifier) -> Result<Option<Column>, Error> {
match self.encrypt.get_column_config(&identifier) {
Some(config) => {
debug!(
Expand All @@ -871,20 +871,24 @@ where
msg = "Configured column",
column = ?identifier
);
Ok(Column::new(identifier, config))
Ok(Some(Column::new(identifier, config)))
}
None => {
debug!(
warn!(
target: MAPPER,
client_id = self.context.client_id,
msg = "Configured column not found ",
msg = "Configured column not found. Encryption configuration may have been deleted.",
?identifier,
);
Err(EncryptError::UnknownColumn {
table: identifier.table.to_owned(),
column: identifier.column.to_owned(),
if self.encrypt.config.mapping_errors_enabled() {
Err(EncryptError::UnknownColumn {
table: identifier.table.to_owned(),
column: identifier.column.to_owned(),
}
.into())
} else {
Ok(None)
}
.into())
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions tests/mise.tcp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ CS_DATABASE__HOST = "postgres"
CS_DATABASE__PORT = "5532"
CS_PROMETHEUS__ENABLED = "true"
CS_LOG__LEVEL = "debug"
CS_DEVELOPMENT__ENABLE_MAPPING_ERRORS = "false"
CS_DEVELOPMENT__DISABLE_MAPPING = "false"
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ def test_encrypted_column_with_no_configuration():

sql = "INSERT INTO unconfigured (id, encrypted_unconfigured) VALUES (%s, %s)"

with pytest.raises(psycopg.Error, match='#encrypt-unknown-column'):
# This is EQL catching the error and returning it. Details are in docs/errors.md
# When mapping errors are enabled, (enable_mapping_errors or CS_DEVELOPMENT__ENABLE_MAPPING_ERRORS)
# Proxy will return an error that says "Column X in table Y has no Encrypt configuration"
with pytest.raises(psycopg.Error, match=r"Encrypted column missing \w+ \(\w+\) field"):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Similarly to the other test above, we show EQL's error here when trying to insert, as we emit the warning and let the unencrypted record pass through to be caught by Postgres.

cursor.execute(sql, [id, val])


Expand Down
24 changes: 24 additions & 0 deletions tests/tasks/test/integration/warn_missing_config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
#MISE description="Test for warning about missing encrypt config. Run with mapping enabled and mapping error disabled"

# This test assumes that Proxy is running with mapping enabled with mapping error disabled

set -e

# Simulate delete config by renaming
docker exec -i postgres"${CONTAINER_SUFFIX}" psql 'postgresql://cipherstash:password@proxy:6432/cipherstash' --command 'ALTER TABLE encrypted RENAME COLUMN encrypted_text TO unconfigured_text;' >/dev/null 2>&1

set +e
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
docker exec -i postgres"${CONTAINER_SUFFIX}" psql 'postgresql://cipherstash:password@proxy:6432/cipherstash?sslmode=disable' --command 'SELECT unconfigured_text from encrypted' >/dev/null 2>&1
LOG_CONTENT=$(docker logs --since "${TIMESTAMP}" proxy | tr "\n" " ")
EXPECTED='Encryption configuration may have been deleted'

# Simulate delete config by renaming
docker exec -i postgres"${CONTAINER_SUFFIX}" psql 'postgresql://cipherstash:password@proxy:6432/cipherstash' --command 'ALTER TABLE encrypted RENAME COLUMN unconfigured_text TO encrypted_text;' >/dev/null 2>&1

if echo "$LOG_CONTENT" | grep -v "${EXPECTED}"; then
echo "error: did not see string in output: \"${EXPECTED}\""
exit 1
fi

Loading