fix: wrap set_encryption_key in a single database transaction (#8212)

Prevent workspace corruption when re-encryption fails mid-loop by
wrapping the key update and variable re-encryption in a single
transaction. If any step fails, the entire operation rolls back.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-03-04 15:53:56 +01:00
committed by GitHub
parent 19c065bed5
commit 62382fd286
3 changed files with 24 additions and 7 deletions
+1
View File
@@ -16383,6 +16383,7 @@ dependencies = [
"http 1.4.0",
"hyper 1.8.1",
"lazy_static",
"magic-crypt",
"regex",
"serde",
"serde_json",
@@ -29,6 +29,7 @@ windmill-dep-map.workspace = true
axum.workspace = true
chrono.workspace = true
hex.workspace = true
magic-crypt.workspace = true
http.workspace = true
hyper.workspace = true
lazy_static.workspace = true
@@ -31,7 +31,9 @@ use windmill_audit::audit_oss::{audit_log, AuditAuthorable};
use windmill_audit::ActionKind;
use windmill_common::db::UserDB;
use windmill_common::users::username_to_permissioned_as;
use windmill_common::variables::{build_crypt, decrypt, encrypt, WORKSPACE_CRYPT_CACHE};
use windmill_common::variables::{
build_crypt, decrypt, encrypt, SECRET_SALT, WORKSPACE_CRYPT_CACHE,
};
use windmill_common::worker::{to_raw_value, CLOUD_HOSTED};
#[cfg(feature = "enterprise")]
use windmill_common::workspaces::GitRepositorySettings;
@@ -2418,20 +2420,28 @@ async fn set_encryption_key(
));
}
// Build the previous cipher before the transaction (reads from cache/pool)
let previous_encryption_key = build_crypt(&db, w_id.as_str()).await?;
let mut tx = db.begin().await?;
sqlx::query!(
"UPDATE workspace_key SET key = $1 WHERE workspace_id = $2",
request.new_key.clone(),
w_id
)
.execute(&db)
.execute(&mut *tx)
.await?;
WORKSPACE_CRYPT_CACHE.remove(w_id.as_str());
if !request.skip_reencrypt.unwrap_or(false) {
let new_encryption_key = build_crypt(&db, w_id.as_str()).await?;
// Build the new cipher directly from the key string, since the transaction
// hasn't committed yet and build_crypt() would read the old key from the pool.
let crypt_key = if let Some(ref salt) = SECRET_SALT.as_ref() {
format!("{}{}", request.new_key, salt)
} else {
request.new_key.clone()
};
let new_encryption_key = magic_crypt::new_magic_crypt!(crypt_key, 256);
let mut truncated_new_key = request.new_key.clone();
truncated_new_key.truncate(8);
@@ -2445,7 +2455,7 @@ async fn set_encryption_key(
"SELECT path, value, is_secret FROM variable WHERE workspace_id = $1",
w_id
)
.fetch_all(&db)
.fetch_all(&mut *tx)
.await?;
for variable in all_variables {
@@ -2466,11 +2476,16 @@ async fn set_encryption_key(
w_id,
variable.path
)
.execute(&db)
.execute(&mut *tx)
.await?;
}
}
tx.commit().await?;
// Invalidate the cache only after the transaction has committed
WORKSPACE_CRYPT_CACHE.remove(w_id.as_str());
// Trigger git sync for encryption key changes
handle_deployment_metadata(
&authed.email,