This is an automated email from the ASF dual-hosted git repository.

hubcio 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 3b9ba2f93 fix(security): require read_servers permission for system 
snapshot (#3579)
3b9ba2f93 is described below

commit 3b9ba2f9340cffc077c9ea39d01d7e8c28be05d0
Author: Hubert Gruszecki <[email protected]>
AuthorDate: Mon Jun 29 12:00:22 2026 +0200

    fix(security): require read_servers permission for system snapshot (#3579)
---
 .../tests/server/scenarios/permissions_scenario.rs | 26 ++++++++++++++++++++++
 .../binary/handlers/system/get_snapshot_handler.rs |  1 +
 core/server/src/http/system.rs                     |  7 +++++-
 core/server/src/metadata/reader.rs                 |  4 ++++
 4 files changed, 37 insertions(+), 1 deletion(-)

diff --git a/core/integration/tests/server/scenarios/permissions_scenario.rs 
b/core/integration/tests/server/scenarios/permissions_scenario.rs
index b9eac9f23..f16c168f3 100644
--- a/core/integration/tests/server/scenarios/permissions_scenario.rs
+++ b/core/integration/tests/server/scenarios/permissions_scenario.rs
@@ -213,6 +213,18 @@ async fn test_no_permissions(harness: &TestHarness, 
root_client: &IggyClient) {
             .await,
         "poll_messages",
     );
+    // Snapshot returns a real archive, never Ok(None), so a denied caller must
+    // get an explicit Unauthorized rather than the enumeration-safe Ok of 
reads.
+    let snapshot_result = client
+        .snapshot(
+            SnapshotCompression::Deflated,
+            vec![SystemSnapshotType::Test],
+        )
+        .await;
+    assert!(
+        matches!(&snapshot_result, Err(e) if e.as_code() == 
IggyError::Unauthorized.as_code()),
+        "snapshot must be rejected as unauthorized without read_servers, got 
{snapshot_result:?}"
+    );
 
     delete_test_user(root_client, USER).await;
 }
@@ -246,6 +258,13 @@ async fn test_system_permissions(harness: &TestHarness, 
root_client: &IggyClient
         .get_clients()
         .await
         .expect("read_servers: get_clients should work");
+    client
+        .snapshot(
+            SnapshotCompression::Deflated,
+            vec![SystemSnapshotType::Test],
+        )
+        .await
+        .expect("read_servers: snapshot should work");
 
     // But cannot read users or streams
     assert_unauthorized(client.get_users().await, "read_servers: get_users");
@@ -277,6 +296,13 @@ async fn test_system_permissions(harness: &TestHarness, 
root_client: &IggyClient
         .get_clients()
         .await
         .expect("manage_servers: get_clients should work");
+    client
+        .snapshot(
+            SnapshotCompression::Deflated,
+            vec![SystemSnapshotType::Test],
+        )
+        .await
+        .expect("manage_servers: snapshot should work");
 
     delete_test_user(root_client, MANAGE_USER).await;
 }
diff --git a/core/server/src/binary/handlers/system/get_snapshot_handler.rs 
b/core/server/src/binary/handlers/system/get_snapshot_handler.rs
index 73cdda2d9..132613f6a 100644
--- a/core/server/src/binary/handlers/system/get_snapshot_handler.rs
+++ b/core/server/src/binary/handlers/system/get_snapshot_handler.rs
@@ -33,6 +33,7 @@ pub async fn handle_get_snapshot(
 ) -> Result<HandlerResult, IggyError> {
     debug!("session: {session}, command: get_snapshot");
     shard.ensure_authenticated(session)?;
+    shard.metadata.perm_get_snapshot(session.get_user_id())?;
 
     let compression = SnapshotCompression::from_code(req.compression)?;
     let snapshot_types: Vec<SystemSnapshotType> = req
diff --git a/core/server/src/http/system.rs b/core/server/src/http/system.rs
index 507613db8..7d8e26b94 100644
--- a/core/server/src/http/system.rs
+++ b/core/server/src/http/system.rs
@@ -127,9 +127,14 @@ async fn get_clients(
 #[debug_handler]
 async fn get_snapshot(
     State(state): State<Arc<AppState>>,
-    Extension(_identity): Extension<Identity>,
+    Extension(identity): Extension<Identity>,
     Json(command): Json<GetSnapshot>,
 ) -> Result<impl IntoResponse, CustomError> {
+    state
+        .shard
+        .shard()
+        .metadata
+        .perm_get_snapshot(identity.user_id)?;
     if command.snapshot_types.contains(&SystemSnapshotType::All) && 
command.snapshot_types.len() > 1
     {
         error!("When using 'All' snapshot type, no other types can be 
specified");
diff --git a/core/server/src/metadata/reader.rs 
b/core/server/src/metadata/reader.rs
index 59d6fcd0b..a5a5ed29a 100644
--- a/core/server/src/metadata/reader.rs
+++ b/core/server/src/metadata/reader.rs
@@ -1290,6 +1290,10 @@ impl Metadata {
         self.perm_get_server_info(user_id)
     }
 
+    pub fn perm_get_snapshot(&self, user_id: u32) -> Result<(), IggyError> {
+        self.perm_get_server_info(user_id)
+    }
+
     fn perm_get_server_info(&self, user_id: u32) -> Result<(), IggyError> {
         let metadata = self.load();
 

Reply via email to