fix: Allow non-admin users to read bucket quota configuration. (#1759)

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <30542132+heihutu@users.noreply.github.com>
This commit is contained in:
yxrxy
2026-02-10 11:23:21 +08:00
committed by GitHub
parent aa011ade19
commit da58f8e291
4 changed files with 63 additions and 8 deletions

View File

@@ -422,7 +422,7 @@ mod integration_tests {
// Check if we can upload 1KB (should succeed - we haven't used the full quota yet)
let check_result = env.check_bucket_quota("PUT", 1024).await?;
assert!(check_result.get("allowed").unwrap().as_bool().unwrap());
assert_eq!(check_result.get("remaining_quota").unwrap().as_u64().unwrap(), 512 * 1024 - 1024);
assert_eq!(check_result.get("remaining_quota").unwrap().as_u64().unwrap(), 523264); // 511 * 1024
// Check if we can upload 600KB (should fail - would exceed quota)
let check_result = env.check_bucket_quota("PUT", 600 * 1024).await?;
@@ -570,6 +570,60 @@ mod integration_tests {
Ok(())
}
/// Test that a normal user with `readwrite` policy can read quota but cannot set/clear quota.
#[tokio::test]
#[serial]
async fn test_quota_normal_user_permissions() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let env = QuotaTestEnv::new().await?;
env.create_bucket().await?;
// Admin sets quota first
env.set_bucket_quota(1024 * 1024).await?;
// Create a normal user via admin API
let normal_ak = "normaluser";
let normal_sk = "normaluser123";
let add_user_url = format!("{}/rustfs/admin/v3/add-user?accessKey={}", env.env.url, normal_ak);
let user_body = serde_json::json!({ "secretKey": normal_sk, "status": "enabled" }).to_string();
awscurl_put(&add_user_url, &user_body, &env.env.access_key, &env.env.secret_key).await?;
// Attach `readwrite` policy to the normal user
let policy_url = format!(
"{}/rustfs/admin/v3/set-user-or-group-policy?policyName=readwrite&userOrGroup={}&isGroup=false",
env.env.url, normal_ak
);
awscurl_put(&policy_url, "", &env.env.access_key, &env.env.secret_key).await?;
// Normal user reads quota — should succeed
let get_url = format!("{}/rustfs/admin/v3/quota/{}", env.env.url, env.bucket_name);
let resp = awscurl_get(&get_url, normal_ak, normal_sk).await?;
let quota_info: serde_json::Value = serde_json::from_str(&resp)?;
assert_eq!(quota_info.get("quota").and_then(|v| v.as_u64()), Some(1024 * 1024));
// Normal user reads quota stats — should succeed
let stats_url = format!("{}/rustfs/admin/v3/quota-stats/{}", env.env.url, env.bucket_name);
let resp = awscurl_get(&stats_url, normal_ak, normal_sk).await?;
assert!(resp.contains("quota_limit"));
// Normal user sets quota — should be denied
let set_resp = awscurl_put(
&get_url,
&serde_json::json!({"quota": 2048, "quota_type": "HARD"}).to_string(),
normal_ak,
normal_sk,
)
.await;
assert!(set_resp.is_err(), "normal user should not be able to set quota");
// Normal user clears quota — should be denied
let del_resp = awscurl_delete(&get_url, normal_ak, normal_sk).await;
assert!(del_resp.is_err(), "normal user should not be able to clear quota");
env.cleanup_bucket().await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn test_quota_copy_operations() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {

View File

@@ -326,6 +326,9 @@ pub enum S3Action {
ResetBucketReplicationStateAction,
#[strum(serialize = "s3:PutObjectFanOut")]
PutObjectFanOutAction,
/// Allow non-admin users to read bucket quota configuration.
#[strum(serialize = "s3:GetBucketQuota")]
GetBucketQuotaAction,
}
// #[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, EnumString, IntoStaticStr, Debug, Copy)]
@@ -469,8 +472,6 @@ pub enum AdminAction {
ListUserPoliciesAdminAction,
#[strum(serialize = "admin:SetBucketQuota")]
SetBucketQuotaAdminAction,
#[strum(serialize = "admin:GetBucketQuota")]
GetBucketQuotaAdminAction,
#[strum(serialize = "admin:SetBucketTarget")]
SetBucketTargetAction,
#[strum(serialize = "admin:GetBucketTarget")]
@@ -559,7 +560,6 @@ impl AdminAction {
| AdminAction::UpdatePolicyAssociationAction
| AdminAction::ListUserPoliciesAdminAction
| AdminAction::SetBucketQuotaAdminAction
| AdminAction::GetBucketQuotaAdminAction
| AdminAction::SetBucketTargetAction
| AdminAction::GetBucketTargetAction
| AdminAction::ReplicationDiff

View File

@@ -320,6 +320,7 @@ pub mod default {
let mut hash_set = HashSet::new();
hash_set.insert(Action::S3Action(S3Action::GetBucketLocationAction));
hash_set.insert(Action::S3Action(S3Action::GetObjectAction));
hash_set.insert(Action::S3Action(S3Action::GetBucketQuotaAction));
hash_set
}),
not_actions: ActionSet(Default::default()),

View File

@@ -21,7 +21,7 @@ use hyper::StatusCode;
use matchit::Params;
use rustfs_ecstore::bucket::quota::checker::QuotaChecker;
use rustfs_ecstore::bucket::quota::{BucketQuota, QuotaError, QuotaOperation};
use rustfs_policy::policy::action::{Action, AdminAction};
use rustfs_policy::policy::action::{Action, AdminAction, S3Action};
use s3s::{Body, S3Request, S3Response, S3Result, s3_error};
use serde::{Deserialize, Serialize};
use serde_json;
@@ -185,7 +185,7 @@ impl Operation for GetBucketQuotaHandler {
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::GetBucketQuotaAdminAction)],
vec![Action::S3Action(S3Action::GetBucketQuotaAction)],
None,
)
.await?;
@@ -313,7 +313,7 @@ impl Operation for GetBucketQuotaStatsHandler {
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::GetBucketQuotaAdminAction)],
vec![Action::S3Action(S3Action::GetBucketQuotaAction)],
None,
)
.await?;
@@ -380,7 +380,7 @@ impl Operation for CheckBucketQuotaHandler {
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::GetBucketQuotaAdminAction)],
vec![Action::S3Action(S3Action::GetBucketQuotaAction)],
None,
)
.await?;