From 5937ae8112131ea96f266faf0a4264c7b34ba84b Mon Sep 17 00:00:00 2001 From: CleverWild Date: Mon, 7 Sep 2026 14:35:04 +0200 Subject: [PATCH] fix(operator): revoke wallet access by access id instead of wallet id --- .../src/peers/operator/session/handlers.rs | 144 ++++++++++++++++-- 1 file changed, 132 insertions(+), 12 deletions(-) diff --git a/server/crates/arbiter-server/src/peers/operator/session/handlers.rs b/server/crates/arbiter-server/src/peers/operator/session/handlers.rs index 6fc8d59..299254a 100644 --- a/server/crates/arbiter-server/src/peers/operator/session/handlers.rs +++ b/server/crates/arbiter-server/src/peers/operator/session/handlers.rs @@ -200,18 +200,7 @@ impl OperatorSession { entries: Vec, ) -> Result<(), Error> { let mut conn = self.props.db.get().await?; - conn.transaction(async |conn| { - use crate::db::schema::evm_wallet_access; - for entry in entries { - diesel::delete(evm_wallet_access::table) - .filter(evm_wallet_access::wallet_id.eq(entry)) - .execute(&mut *conn) - .await?; - } - - Result::<_, Error>::Ok(()) - }) - .await?; + revoke_wallet_access(&mut conn, &entries).await?; Ok(()) } @@ -229,6 +218,20 @@ impl OperatorSession { } } +/// Deletes access rows by their own id. The wire carries `WalletAccessEntry.id` values, so +/// filtering by `wallet_id` here would revoke every client's access to that wallet. +pub(crate) async fn revoke_wallet_access( + conn: &mut crate::db::DatabaseConnection, + ids: &[i32], +) -> Result { + use crate::db::schema::evm_wallet_access; + + diesel::delete(evm_wallet_access::table) + .filter(evm_wallet_access::id.eq_any(ids)) + .execute(conn) + .await +} + #[messages] impl OperatorSession { #[message(ctx)] @@ -379,3 +382,120 @@ impl OperatorSession { .map_err(|_| Error::internal("VaultCoordinator unavailable")) } } + +#[cfg(test)] +mod tests { + use super::revoke_wallet_access; + use crate::db::{self, models, schema}; + + use diesel::{ExpressionMethods as _, QueryDsl as _, dsl::insert_into}; + use diesel_async::RunQueryDsl; + + /// Two clients share one wallet. Revoking one access row must leave the other alone. + #[tokio::test] + async fn revoking_one_access_leaves_the_other_client_alone() { + let pool = db::create_test_pool().await; + let mut conn = pool.get().await.unwrap(); + + let root_key_id: models::RootKeyHistoryId = insert_into(schema::root_key_history::table) + .values(&models::NewRootKeyHistory { + ciphertext: vec![0u8; 32], + tag: vec![0u8; 16], + root_key_encryption_nonce: vec![0u8; 24], + data_encryption_nonce: vec![0u8; 24], + schema_version: 1, + salt: vec![0u8; 16], + }) + .returning(schema::root_key_history::id) + .get_result(&mut conn) + .await + .unwrap(); + + let aead_id: i32 = insert_into(schema::aead_encrypted::table) + .values(&models::NewAeadEncrypted { + ciphertext: vec![0u8; 32], + tag: vec![0u8; 16], + current_nonce: vec![0u8; 24], + schema_version: 1, + associated_root_key_id: root_key_id, + created_at: chrono::Utc::now().into(), + }) + .returning(schema::aead_encrypted::id) + .get_result(&mut conn) + .await + .unwrap(); + + let wallet_id: models::EvmWalletId = insert_into(schema::evm_wallet::table) + .values(( + schema::evm_wallet::address.eq(vec![0u8; 20]), + schema::evm_wallet::aead_encrypted_id.eq(aead_id), + )) + .returning(schema::evm_wallet::id) + .get_result(&mut conn) + .await + .unwrap(); + + let metadata_id: i32 = insert_into(schema::client_metadata::table) + .values(schema::client_metadata::name.eq("test")) + .returning(schema::client_metadata::id) + .get_result(&mut conn) + .await + .unwrap(); + + let first_client: i32 = insert_into(schema::program_client::table) + .values(( + schema::program_client::public_key.eq(vec![1u8; 32]), + schema::program_client::metadata_id.eq(metadata_id), + )) + .returning(schema::program_client::id) + .get_result(&mut conn) + .await + .unwrap(); + + let second_client: i32 = insert_into(schema::program_client::table) + .values(( + schema::program_client::public_key.eq(vec![2u8; 32]), + schema::program_client::metadata_id.eq(metadata_id), + )) + .returning(schema::program_client::id) + .get_result(&mut conn) + .await + .unwrap(); + + let first_access: i32 = insert_into(schema::evm_wallet_access::table) + .values(( + schema::evm_wallet_access::wallet_id.eq(wallet_id), + schema::evm_wallet_access::client_id.eq(first_client), + )) + .returning(schema::evm_wallet_access::id) + .get_result(&mut conn) + .await + .unwrap(); + + let _second_access: i32 = insert_into(schema::evm_wallet_access::table) + .values(( + schema::evm_wallet_access::wallet_id.eq(wallet_id), + schema::evm_wallet_access::client_id.eq(second_client), + )) + .returning(schema::evm_wallet_access::id) + .get_result(&mut conn) + .await + .unwrap(); + + let removed = revoke_wallet_access(&mut conn, &[first_access]) + .await + .unwrap(); + assert_eq!(removed, 1); + + let survivors: Vec = schema::evm_wallet_access::table + .select(schema::evm_wallet_access::client_id) + .load(&mut conn) + .await + .unwrap(); + assert_eq!( + survivors, + vec![second_client], + "revoking one access row removed another client's access" + ); + } +}