This is an automated email from the ASF dual-hosted git repository.
piotr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/master by this push:
new 8b5a775e6 fix(server): hash passwords before persisting to state log
(#2724)
8b5a775e6 is described below
commit 8b5a775e63d9c9f0bf5b5828e87e50db44609667
Author: Piotr Gankiewicz <[email protected]>
AuthorDate: Thu Feb 12 06:57:22 2026 +0100
fix(server): hash passwords before persisting to state log (#2724)
The shard and HTTP handlers for CreateUser and ChangePassword
stored plaintext passwords in the state log instead of Argon2
hashes. On restart, state replay loaded the plaintext as a
password hash, causing PasswordHash::new() to panic with
"Failed to parse password hash: PhcStringField".
The root user was unaffected because its password was hashed
before being written to the state entry.
Fixes #2720
---
core/integration/tests/data_integrity/mod.rs | 1 +
.../verify_user_login_after_restart.rs | 65 ++++++++++++++++++++++
core/server/src/http/users.rs | 6 +-
core/server/src/shard/handlers.rs | 7 ++-
4 files changed, 75 insertions(+), 4 deletions(-)
diff --git a/core/integration/tests/data_integrity/mod.rs
b/core/integration/tests/data_integrity/mod.rs
index 5cf911c94..742e3128e 100644
--- a/core/integration/tests/data_integrity/mod.rs
+++ b/core/integration/tests/data_integrity/mod.rs
@@ -17,3 +17,4 @@
*/
mod verify_after_server_restart;
+mod verify_user_login_after_restart;
diff --git
a/core/integration/tests/data_integrity/verify_user_login_after_restart.rs
b/core/integration/tests/data_integrity/verify_user_login_after_restart.rs
new file mode 100644
index 000000000..d6015f420
--- /dev/null
+++ b/core/integration/tests/data_integrity/verify_user_login_after_restart.rs
@@ -0,0 +1,65 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use iggy::prelude::*;
+use integration::harness::{USER_PASSWORD, create_user, login_user};
+use integration::iggy_harness;
+
+#[iggy_harness(test_client_transport = [Tcp, Http, Quic, WebSocket])]
+async fn should_login_non_root_user_after_restart(harness: &mut TestHarness) {
+ let root_client = harness.root_client().await.unwrap();
+ create_user(&root_client, "testuser").await;
+
+ let client = harness.new_client().await.unwrap();
+ login_user(&client, "testuser").await;
+ drop(client);
+ drop(root_client);
+
+ harness.restart_server().await.unwrap();
+
+ let root_client = harness.root_client().await.unwrap();
+ let users = root_client.get_users().await.unwrap();
+ assert_eq!(users.len(), 2, "Expected root + testuser after restart");
+ drop(root_client);
+
+ let client = harness.new_client().await.unwrap();
+ login_user(&client, "testuser").await;
+}
+
+#[iggy_harness(test_client_transport = [Tcp, Http, Quic, WebSocket])]
+async fn should_login_after_password_change_and_restart(harness: &mut
TestHarness) {
+ let root_client = harness.root_client().await.unwrap();
+ create_user(&root_client, "testuser").await;
+
+ let new_password = "new_secret_password";
+ let user_id = Identifier::named("testuser").unwrap();
+ root_client
+ .change_password(&user_id, USER_PASSWORD, new_password)
+ .await
+ .unwrap();
+
+ let client = harness.new_client().await.unwrap();
+ client.login_user("testuser", new_password).await.unwrap();
+ drop(client);
+ drop(root_client);
+
+ harness.restart_server().await.unwrap();
+
+ let client = harness.new_client().await.unwrap();
+ client.login_user("testuser", new_password).await.unwrap();
+}
diff --git a/core/server/src/http/users.rs b/core/server/src/http/users.rs
index 8e4dcd1b9..b2b0c1fba 100644
--- a/core/server/src/http/users.rs
+++ b/core/server/src/http/users.rs
@@ -298,7 +298,11 @@ async fn change_password(
})?;
{
- let entry_command = EntryCommand::ChangePassword(command);
+ let entry_command = EntryCommand::ChangePassword(ChangePassword {
+ user_id: command.user_id,
+ current_password: "".into(),
+ new_password: crypto::hash_password(&command.new_password),
+ });
let future = SendWrapper::new(
state
.shard
diff --git a/core/server/src/shard/handlers.rs
b/core/server/src/shard/handlers.rs
index 53756d5f8..f48e3f91c 100644
--- a/core/server/src/shard/handlers.rs
+++ b/core/server/src/shard/handlers.rs
@@ -26,6 +26,7 @@ use crate::{
message::{ShardMessage, ShardRequest, ShardRequestPayload},
},
},
+ streaming::utils::crypto,
tcp::{
connection_handler::{ConnectionAction, handle_connection,
handle_error},
tcp_listener::cleanup_connection,
@@ -406,7 +407,7 @@ async fn handle_request(
let command = iggy_common::create_user::CreateUser {
username,
- password,
+ password: crypto::hash_password(&password),
status,
permissions,
};
@@ -560,8 +561,8 @@ async fn handle_request(
let command = iggy_common::change_password::ChangePassword {
user_id,
- current_password,
- new_password,
+ current_password: "".into(),
+ new_password: crypto::hash_password(&new_password),
};
shard
.state