fix: correctly handle aws:SourceIp in policy evaluation (#1301) (#1306)

Signed-off-by: loverustfs <github@rustfs.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
loverustfs
2025-12-30 16:54:48 +08:00
committed by GitHub
parent a5b3522880
commit b4ba62fa33
19 changed files with 371 additions and 37 deletions

View File

@@ -82,7 +82,7 @@ tokio-stream.workspace = true
tokio-util.workspace = true
tonic = { workspace = true }
tower.workspace = true
tower-http = { workspace = true, features = ["trace", "compression-full", "cors", "catch-panic", "timeout", "limit", "request-id"] }
tower-http = { workspace = true, features = ["trace", "compression-full", "cors", "catch-panic", "timeout", "limit", "request-id", "add-extension"] }
# Serialization and Data Formats
bytes = { workspace = true }

View File

@@ -30,12 +30,13 @@ pub async fn validate_admin_request(
is_owner: bool,
deny_only: bool,
actions: Vec<Action>,
remote_addr: Option<std::net::SocketAddr>,
) -> S3Result<()> {
let Ok(iam_store) = rustfs_iam::get() else {
return Err(s3_error!(InternalError, "iam not init"));
};
for action in actions {
match check_admin_request_auth(iam_store.clone(), headers, cred, is_owner, deny_only, action).await {
match check_admin_request_auth(iam_store.clone(), headers, cred, is_owner, deny_only, action, remote_addr).await {
Ok(_) => return Ok(()),
Err(_) => {
continue;
@@ -53,8 +54,9 @@ async fn check_admin_request_auth(
is_owner: bool,
deny_only: bool,
action: Action,
remote_addr: Option<std::net::SocketAddr>,
) -> S3Result<()> {
let conditions = get_condition_values(headers, cred, None, None);
let conditions = get_condition_values(headers, cred, None, None, remote_addr);
if !iam_store
.is_allowed(&Args {

View File

@@ -18,6 +18,7 @@ use crate::auth::check_key_valid;
use crate::auth::get_condition_values;
use crate::auth::get_session_token;
use crate::error::ApiError;
use crate::server::RemoteAddr;
use bytes::Bytes;
use futures::{Stream, StreamExt};
use http::{HeaderMap, HeaderValue, Uri};
@@ -210,7 +211,8 @@ impl Operation for AccountInfoHandler {
let claims = cred.claims.as_ref().unwrap_or(&default_claims);
let cred_clone = cred.clone();
let conditions = get_condition_values(&req.headers, &cred_clone, None, None);
let remote_addr = req.extensions.get::<RemoteAddr>().map(|a| a.0);
let conditions = get_condition_values(&req.headers, &cred_clone, None, None, remote_addr);
let cred_clone = Arc::new(cred_clone);
let conditions = Arc::new(conditions);
@@ -405,12 +407,14 @@ impl Operation for ServerInfoHandler {
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
let remote_addr = req.extensions.get::<RemoteAddr>().map(|a| a.0);
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
remote_addr,
)
.await?;
@@ -451,12 +455,14 @@ impl Operation for StorageInfoHandler {
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
let remote_addr = req.extensions.get::<RemoteAddr>().map(|a| a.0);
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::StorageInfoAdminAction)],
remote_addr,
)
.await?;
@@ -492,6 +498,7 @@ impl Operation for DataUsageInfoHandler {
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
let remote_addr = req.extensions.get::<RemoteAddr>().map(|a| a.0);
validate_admin_request(
&req.headers,
&cred,
@@ -501,6 +508,7 @@ impl Operation for DataUsageInfoHandler {
Action::AdminAction(AdminAction::DataUsageInfoAdminAction),
Action::S3Action(S3Action::ListBucketAction),
],
remote_addr,
)
.await?;

View File

@@ -20,6 +20,7 @@ use std::{
use crate::{
admin::{auth::validate_admin_request, router::Operation},
auth::{check_key_valid, get_session_token},
server::RemoteAddr,
};
use http::{HeaderMap, StatusCode};
use matchit::Params;
@@ -97,6 +98,7 @@ impl Operation for ExportBucketMetadata {
owner,
false,
vec![Action::AdminAction(AdminAction::ExportBucketMetadataAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -389,6 +391,7 @@ impl Operation for ImportBucketMetadata {
owner,
false,
vec![Action::AdminAction(AdminAction::ImportBucketMetadataAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;

View File

@@ -15,6 +15,7 @@
use crate::{
admin::{auth::validate_admin_request, router::Operation, utils::has_space_be},
auth::{check_key_valid, constant_time_eq, get_session_token},
server::RemoteAddr,
};
use http::{HeaderMap, StatusCode};
use matchit::Params;
@@ -57,6 +58,7 @@ impl Operation for ListGroups {
owner,
false,
vec![Action::AdminAction(AdminAction::ListGroupsAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -95,6 +97,7 @@ impl Operation for GetGroup {
owner,
false,
vec![Action::AdminAction(AdminAction::GetGroupAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -142,6 +145,7 @@ impl Operation for SetGroupStatus {
owner,
false,
vec![Action::AdminAction(AdminAction::EnableGroupAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -209,6 +213,7 @@ impl Operation for UpdateGroupMembers {
owner,
false,
vec![Action::AdminAction(AdminAction::AddUserToGroupAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;

View File

@@ -17,6 +17,7 @@
use super::Operation;
use crate::admin::auth::validate_admin_request;
use crate::auth::{check_key_valid, get_session_token};
use crate::server::RemoteAddr;
use base64::Engine;
use hyper::{HeaderMap, StatusCode};
use matchit::Params;
@@ -127,6 +128,7 @@ impl Operation for CreateKeyHandler {
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)], // TODO: Add specific KMS action
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -205,6 +207,7 @@ impl Operation for DescribeKeyHandler {
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -260,6 +263,7 @@ impl Operation for ListKeysHandler {
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -321,6 +325,7 @@ impl Operation for GenerateDataKeyHandler {
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -386,6 +391,7 @@ impl Operation for KmsStatusHandler {
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -443,6 +449,7 @@ impl Operation for KmsConfigHandler {
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -487,6 +494,7 @@ impl Operation for KmsClearCacheHandler {
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;

View File

@@ -17,6 +17,7 @@
use super::Operation;
use crate::admin::auth::validate_admin_request;
use crate::auth::{check_key_valid, get_session_token};
use crate::server::RemoteAddr;
use hyper::StatusCode;
use matchit::Params;
use rustfs_config::MAX_ADMIN_REQUEST_BODY_SIZE;
@@ -98,6 +99,7 @@ impl Operation for ConfigureKmsHandler {
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -196,6 +198,7 @@ impl Operation for StartKmsHandler {
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -329,6 +332,7 @@ impl Operation for StopKmsHandler {
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -394,6 +398,7 @@ impl Operation for GetKmsStatusHandler {
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -465,6 +470,7 @@ impl Operation for ReconfigureKmsHandler {
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;

View File

@@ -17,6 +17,7 @@
use super::Operation;
use crate::admin::auth::validate_admin_request;
use crate::auth::{check_key_valid, get_session_token};
use crate::server::RemoteAddr;
use hyper::{HeaderMap, StatusCode};
use matchit::Params;
use rustfs_config::MAX_ADMIN_REQUEST_BODY_SIZE;
@@ -79,6 +80,7 @@ impl Operation for CreateKmsKeyHandler {
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -212,6 +214,7 @@ impl Operation for DeleteKmsKeyHandler {
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -360,6 +363,7 @@ impl Operation for CancelKmsKeyDeletionHandler {
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -488,6 +492,7 @@ impl Operation for ListKmsKeysHandler {
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -599,6 +604,7 @@ impl Operation for DescribeKmsKeyHandler {
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;

View File

@@ -15,6 +15,7 @@
use crate::{
admin::{auth::validate_admin_request, router::Operation, utils::has_space_be},
auth::{check_key_valid, get_session_token},
server::RemoteAddr,
};
use http::{HeaderMap, StatusCode};
use matchit::Params;
@@ -60,6 +61,7 @@ impl Operation for ListCannedPolicies {
owner,
false,
vec![Action::AdminAction(AdminAction::ListUserPoliciesAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -118,6 +120,7 @@ impl Operation for AddCannedPolicy {
owner,
false,
vec![Action::AdminAction(AdminAction::CreatePolicyAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -190,6 +193,7 @@ impl Operation for InfoCannedPolicy {
owner,
false,
vec![Action::AdminAction(AdminAction::GetPolicyAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -247,6 +251,7 @@ impl Operation for RemoveCannedPolicy {
owner,
false,
vec![Action::AdminAction(AdminAction::DeletePolicyAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -307,6 +312,7 @@ impl Operation for SetPolicyForUserOrGroup {
owner,
false,
vec![Action::AdminAction(AdminAction::AttachPolicyAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;

View File

@@ -26,6 +26,7 @@ use crate::{
admin::{auth::validate_admin_request, router::Operation},
auth::{check_key_valid, get_session_token},
error::ApiError,
server::RemoteAddr,
};
pub struct ListPools {}
@@ -53,6 +54,7 @@ impl Operation for ListPools {
Action::AdminAction(AdminAction::ServerInfoAdminAction),
Action::AdminAction(AdminAction::DecommissionAdminAction),
],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -119,6 +121,7 @@ impl Operation for StatusPool {
Action::AdminAction(AdminAction::ServerInfoAdminAction),
Action::AdminAction(AdminAction::DecommissionAdminAction),
],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -194,6 +197,7 @@ impl Operation for StartDecommission {
owner,
false,
vec![Action::AdminAction(AdminAction::DecommissionAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -292,6 +296,7 @@ impl Operation for CancelDecommission {
owner,
false,
vec![Action::AdminAction(AdminAction::DecommissionAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;

View File

@@ -15,6 +15,7 @@
use crate::{
admin::{auth::validate_admin_request, router::Operation},
auth::{check_key_valid, get_session_token},
server::RemoteAddr,
};
use http::{HeaderMap, StatusCode};
use matchit::Params;
@@ -103,6 +104,7 @@ impl Operation for RebalanceStart {
owner,
false,
vec![Action::AdminAction(AdminAction::RebalanceAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -180,6 +182,7 @@ impl Operation for RebalanceStatus {
owner,
false,
vec![Action::AdminAction(AdminAction::RebalanceAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -297,6 +300,7 @@ impl Operation for RebalanceStop {
owner,
false,
vec![Action::AdminAction(AdminAction::RebalanceAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;

View File

@@ -14,6 +14,7 @@
use crate::admin::utils::has_space_be;
use crate::auth::{constant_time_eq, get_condition_values, get_session_token};
use crate::server::RemoteAddr;
use crate::{admin::router::Operation, auth::check_key_valid};
use http::HeaderMap;
use hyper::StatusCode;
@@ -119,7 +120,13 @@ impl Operation for AddServiceAccount {
groups: &cred.groups,
action: Action::AdminAction(AdminAction::CreateServiceAccountAdminAction),
bucket: "",
conditions: &get_condition_values(&req.headers, &cred, None, None),
conditions: &get_condition_values(
&req.headers,
&cred,
None,
None,
req.extensions.get::<RemoteAddr>().map(|a| a.0),
),
is_owner: owner,
object: "",
claims: cred.claims.as_ref().unwrap_or(&HashMap::new()),
@@ -270,7 +277,13 @@ impl Operation for UpdateServiceAccount {
groups: &cred.groups,
action: Action::AdminAction(AdminAction::UpdateServiceAccountAdminAction),
bucket: "",
conditions: &get_condition_values(&req.headers, &cred, None, None),
conditions: &get_condition_values(
&req.headers,
&cred,
None,
None,
req.extensions.get::<RemoteAddr>().map(|a| a.0),
),
is_owner: owner,
object: "",
claims: cred.claims.as_ref().unwrap_or(&HashMap::new()),
@@ -363,7 +376,13 @@ impl Operation for InfoServiceAccount {
groups: &cred.groups,
action: Action::AdminAction(AdminAction::ListServiceAccountsAdminAction),
bucket: "",
conditions: &get_condition_values(&req.headers, &cred, None, None),
conditions: &get_condition_values(
&req.headers,
&cred,
None,
None,
req.extensions.get::<RemoteAddr>().map(|a| a.0),
),
is_owner: owner,
object: "",
claims: cred.claims.as_ref().unwrap_or(&HashMap::new()),
@@ -491,7 +510,13 @@ impl Operation for ListServiceAccount {
groups: &cred.groups,
action: Action::AdminAction(AdminAction::UpdateServiceAccountAdminAction),
bucket: "",
conditions: &get_condition_values(&req.headers, &cred, None, None),
conditions: &get_condition_values(
&req.headers,
&cred,
None,
None,
req.extensions.get::<RemoteAddr>().map(|a| a.0),
),
is_owner: owner,
object: "",
claims: cred.claims.as_ref().unwrap_or(&HashMap::new()),
@@ -589,7 +614,13 @@ impl Operation for DeleteServiceAccount {
groups: &cred.groups,
action: Action::AdminAction(AdminAction::RemoveServiceAccountAdminAction),
bucket: "",
conditions: &get_condition_values(&req.headers, &cred, None, None),
conditions: &get_condition_values(
&req.headers,
&cred,
None,
None,
req.extensions.get::<RemoteAddr>().map(|a| a.0),
),
is_owner: owner,
object: "",
claims: cred.claims.as_ref().unwrap_or(&HashMap::new()),

View File

@@ -16,6 +16,7 @@
use crate::{
admin::{auth::validate_admin_request, router::Operation},
auth::{check_key_valid, get_session_token},
server::RemoteAddr,
};
use http::{HeaderMap, StatusCode};
use matchit::Params;
@@ -90,7 +91,15 @@ impl Operation for AddTier {
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(AdminAction::SetTierAction)]).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::SetTierAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
let mut input = req.input;
let body = match input.store_all_limited(MAX_ADMIN_REQUEST_BODY_SIZE).await {
@@ -218,7 +227,15 @@ impl Operation for EditTier {
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(AdminAction::SetTierAction)]).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::SetTierAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
let mut input = req.input;
let body = match input.store_all_limited(MAX_ADMIN_REQUEST_BODY_SIZE).await {
@@ -293,7 +310,15 @@ impl Operation for ListTiers {
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(AdminAction::ListTierAction)]).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::ListTierAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
let mut tier_config_mgr = GLOBAL_TierConfigMgr.read().await;
let tiers = tier_config_mgr.list_tiers();
@@ -329,7 +354,15 @@ impl Operation for RemoveTier {
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(AdminAction::SetTierAction)]).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::SetTierAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
let mut force: bool = false;
let force_str = query.force.clone().unwrap_or_default();
@@ -392,7 +425,15 @@ impl Operation for VerifyTier {
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(AdminAction::ListTierAction)]).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::ListTierAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
let mut tier_config_mgr = GLOBAL_TierConfigMgr.write().await;
tier_config_mgr.verify(&query.tier.unwrap()).await;
@@ -415,7 +456,15 @@ impl Operation for GetTierInfo {
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(AdminAction::ListTierAction)]).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::ListTierAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
let query = {
if let Some(query) = req.uri.query() {
@@ -467,7 +516,15 @@ impl Operation for ClearTier {
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(AdminAction::SetTierAction)]).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::SetTierAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
let mut force: bool = false;
let force_str = query.force;

View File

@@ -15,6 +15,7 @@
use crate::{
admin::{auth::validate_admin_request, router::Operation, utils::has_space_be},
auth::{check_key_valid, constant_time_eq, get_session_token},
server::RemoteAddr,
};
use http::{HeaderMap, StatusCode};
use matchit::Params;
@@ -124,6 +125,7 @@ impl Operation for AddUser {
owner,
deny_only,
vec![Action::AdminAction(AdminAction::CreateUserAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -176,6 +178,7 @@ impl Operation for SetUserStatus {
owner,
false,
vec![Action::AdminAction(AdminAction::EnableUserAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -220,6 +223,7 @@ impl Operation for ListUsers {
owner,
false,
vec![Action::AdminAction(AdminAction::ListUsersAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -278,6 +282,7 @@ impl Operation for RemoveUser {
owner,
false,
vec![Action::AdminAction(AdminAction::DeleteUserAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -377,6 +382,7 @@ impl Operation for GetUserInfo {
owner,
deny_only,
vec![Action::AdminAction(AdminAction::GetUserAdminAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
@@ -426,8 +432,15 @@ impl Operation for ExportIam {
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(AdminAction::ExportIAMAction)])
.await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::ExportIAMAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
let Ok(iam_store) = rustfs_iam::get() else {
return Err(s3_error!(InvalidRequest, "iam not init"));
@@ -633,8 +646,15 @@ impl Operation for ImportIam {
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(AdminAction::ExportIAMAction)])
.await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::ExportIAMAction)],
req.extensions.get::<RemoteAddr>().map(|a| a.0),
)
.await?;
let mut input = req.input;
let body = match input.store_all_limited(MAX_IAM_IMPORT_SIZE).await {

View File

@@ -241,6 +241,7 @@ pub fn get_session_token<'a>(uri: &'a Uri, hds: &'a HeaderMap) -> Option<&'a str
/// * `cred` - User credentials
/// * `version_id` - Optional version ID of the object
/// * `region` - Optional region/location constraint
/// * `remote_addr` - Optional remote address of the connection
///
/// # Returns
/// * `HashMap<String, Vec<String>>` - Condition values for policy evaluation
@@ -250,6 +251,7 @@ pub fn get_condition_values(
cred: &Credentials,
version_id: Option<&str>,
region: Option<&str>,
remote_addr: Option<std::net::SocketAddr>,
) -> HashMap<String, Vec<String>> {
let username = if cred.is_temp() || cred.is_service_account() {
cred.parent_user.clone()
@@ -297,12 +299,7 @@ pub fn get_condition_values(
.unwrap_or(false);
// Get remote address from header or use default
let remote_addr = header
.get("x-forwarded-for")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.split(',').next())
.or_else(|| header.get("x-real-ip").and_then(|v| v.to_str().ok()))
.unwrap_or("127.0.0.1");
let remote_addr_s = remote_addr.map(|a| a.ip().to_string()).unwrap_or_default();
let mut args = HashMap::new();
@@ -310,7 +307,7 @@ pub fn get_condition_values(
args.insert("CurrentTime".to_owned(), vec![curr_time.format(&Rfc3339).unwrap_or_default()]);
args.insert("EpochTime".to_owned(), vec![epoch_time.to_string()]);
args.insert("SecureTransport".to_owned(), vec![is_tls.to_string()]);
args.insert("SourceIp".to_owned(), vec![get_source_ip_raw(header, remote_addr)]);
args.insert("SourceIp".to_owned(), vec![get_source_ip_raw(header, &remote_addr_s)]);
// Add user agent and referer
if let Some(user_agent) = header.get("user-agent") {
@@ -848,7 +845,7 @@ mod tests {
let cred = create_test_credentials();
let headers = HeaderMap::new();
let conditions = get_condition_values(&headers, &cred, None, None);
let conditions = get_condition_values(&headers, &cred, None, None, None);
assert_eq!(conditions.get("userid"), Some(&vec!["test-access-key".to_string()]));
assert_eq!(conditions.get("username"), Some(&vec!["test-access-key".to_string()]));
@@ -860,7 +857,7 @@ mod tests {
let cred = create_temp_credentials();
let headers = HeaderMap::new();
let conditions = get_condition_values(&headers, &cred, None, None);
let conditions = get_condition_values(&headers, &cred, None, None, None);
assert_eq!(conditions.get("userid"), Some(&vec!["parent-user".to_string()]));
assert_eq!(conditions.get("username"), Some(&vec!["parent-user".to_string()]));
@@ -872,7 +869,7 @@ mod tests {
let cred = create_service_account_credentials();
let headers = HeaderMap::new();
let conditions = get_condition_values(&headers, &cred, None, None);
let conditions = get_condition_values(&headers, &cred, None, None, None);
assert_eq!(conditions.get("userid"), Some(&vec!["service-parent".to_string()]));
assert_eq!(conditions.get("username"), Some(&vec!["service-parent".to_string()]));
@@ -887,7 +884,7 @@ mod tests {
headers.insert("x-amz-object-lock-mode", HeaderValue::from_static("GOVERNANCE"));
headers.insert("x-amz-object-lock-retain-until-date", HeaderValue::from_static("2024-12-31T23:59:59Z"));
let conditions = get_condition_values(&headers, &cred, None, None);
let conditions = get_condition_values(&headers, &cred, None, None, None);
assert_eq!(conditions.get("object-lock-mode"), Some(&vec!["GOVERNANCE".to_string()]));
assert_eq!(
@@ -902,7 +899,7 @@ mod tests {
let mut headers = HeaderMap::new();
headers.insert("x-amz-signature-age", HeaderValue::from_static("300"));
let conditions = get_condition_values(&headers, &cred, None, None);
let conditions = get_condition_values(&headers, &cred, None, None, None);
assert_eq!(conditions.get("signatureAge"), Some(&vec!["300".to_string()]));
// Verify the header is removed after processing
@@ -919,7 +916,7 @@ mod tests {
let headers = HeaderMap::new();
let conditions = get_condition_values(&headers, &cred, None, None);
let conditions = get_condition_values(&headers, &cred, None, None, None);
assert_eq!(conditions.get("username"), Some(&vec!["ldap-user".to_string()]));
assert_eq!(conditions.get("groups"), Some(&vec!["group1".to_string(), "group2".to_string()]));
@@ -932,7 +929,7 @@ mod tests {
let headers = HeaderMap::new();
let conditions = get_condition_values(&headers, &cred, None, None);
let conditions = get_condition_values(&headers, &cred, None, None, None);
assert_eq!(
conditions.get("groups"),
@@ -1208,4 +1205,159 @@ mod tests {
assert!(constant_time_eq(key1, key2));
assert!(!constant_time_eq(key1, key3));
}
#[test]
fn test_get_condition_values_source_ip() {
let mut headers = HeaderMap::new();
let cred = Credentials::default();
// Case 1: No headers, no remote addr -> empty string
let conditions = get_condition_values(&headers, &cred, None, None, None);
assert_eq!(conditions.get("SourceIp").unwrap()[0], "");
// Case 2: No headers, with remote addr -> remote addr
let remote_addr: std::net::SocketAddr = "192.168.0.10:12345".parse().unwrap();
let conditions = get_condition_values(&headers, &cred, None, None, Some(remote_addr));
assert_eq!(conditions.get("SourceIp").unwrap()[0], "192.168.0.10");
// Case 3: X-Forwarded-For present -> XFF (takes precedence over remote_addr)
headers.insert("x-forwarded-for", HeaderValue::from_static("10.0.0.1"));
let conditions = get_condition_values(&headers, &cred, None, None, Some(remote_addr));
assert_eq!(conditions.get("SourceIp").unwrap()[0], "10.0.0.1");
// Case 4: X-Forwarded-For with multiple IPs -> First IP
headers.insert("x-forwarded-for", HeaderValue::from_static("10.0.0.3, 10.0.0.4"));
let conditions = get_condition_values(&headers, &cred, None, None, Some(remote_addr));
assert_eq!(conditions.get("SourceIp").unwrap()[0], "10.0.0.3");
// Case 5: X-Real-IP present (XFF removed) -> X-Real-IP
headers.remove("x-forwarded-for");
headers.insert("x-real-ip", HeaderValue::from_static("10.0.0.2"));
let conditions = get_condition_values(&headers, &cred, None, None, Some(remote_addr));
assert_eq!(conditions.get("SourceIp").unwrap()[0], "10.0.0.2");
// Case 6: Forwarded header present (X-Real-IP removed) -> Forwarded
headers.remove("x-real-ip");
headers.insert("forwarded", HeaderValue::from_static("for=10.0.0.5;proto=http"));
let conditions = get_condition_values(&headers, &cred, None, None, Some(remote_addr));
assert_eq!(conditions.get("SourceIp").unwrap()[0], "10.0.0.5");
// Case 7: Forwarded header with quotes and multiple values
headers.insert("forwarded", HeaderValue::from_static("for=\"10.0.0.6\", for=10.0.0.7"));
let conditions = get_condition_values(&headers, &cred, None, None, Some(remote_addr));
assert_eq!(conditions.get("SourceIp").unwrap()[0], "10.0.0.6");
// Case 8: IPv6 Remote Addr
let remote_addr_v6: std::net::SocketAddr = "[2001:db8::1]:8080".parse().unwrap();
headers.clear();
let conditions = get_condition_values(&headers, &cred, None, None, Some(remote_addr_v6));
assert_eq!(conditions.get("SourceIp").unwrap()[0], "2001:db8::1");
}
}
#[cfg(test)]
mod tests_policy {
use rustfs_policy::policy::action::{Action, S3Action};
use rustfs_policy::policy::{Args, BucketPolicy, BucketPolicyArgs, Policy};
use std::collections::HashMap;
#[tokio::test]
async fn test_iam_policy_source_ip() {
let policy_json = r#"{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::mybucket/*"],
"Condition": {
"IpAddress": {
"aws:SourceIp": "192.168.1.0/24"
}
}
}
]
}"#;
let policy: Policy = serde_json::from_str(policy_json).expect("Failed to parse IAM policy");
// Case 1: Matching IP
let mut conditions = HashMap::new();
conditions.insert("SourceIp".to_string(), vec!["192.168.1.10".to_string()]);
let claims = HashMap::new();
let args = Args {
account: "test-account",
groups: &None,
action: Action::S3Action(S3Action::GetObjectAction),
bucket: "mybucket",
conditions: &conditions,
is_owner: false,
object: "myobject",
claims: &claims,
deny_only: false,
};
assert!(policy.is_allowed(&args).await, "IAM Policy should allow matching IP");
// Case 2: Non-matching IP
let mut conditions_fail = HashMap::new();
conditions_fail.insert("SourceIp".to_string(), vec!["10.0.0.1".to_string()]);
let args_fail = Args {
conditions: &conditions_fail,
..args
};
assert!(!policy.is_allowed(&args_fail).await, "IAM Policy should deny non-matching IP");
}
#[tokio::test]
async fn test_bucket_policy_source_ip() {
let policy_json = r#"{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"AWS": ["*"]},
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::mybucket/*"],
"Condition": {
"IpAddress": {
"aws:SourceIp": "192.168.1.0/24"
}
}
}
]
}"#;
let policy: BucketPolicy = serde_json::from_str(policy_json).expect("Failed to parse Bucket policy");
// Case 1: Matching IP
let mut conditions = HashMap::new();
conditions.insert("SourceIp".to_string(), vec!["192.168.1.10".to_string()]);
let args = BucketPolicyArgs {
account: "test-account",
groups: &None,
action: Action::S3Action(S3Action::GetObjectAction),
bucket: "mybucket",
conditions: &conditions,
is_owner: false,
object: "myobject",
};
assert!(policy.is_allowed(&args).await, "Bucket Policy should allow matching IP");
// Case 2: Non-matching IP
let mut conditions_fail = HashMap::new();
conditions_fail.insert("SourceIp".to_string(), vec!["10.0.0.1".to_string()]);
let args_fail = BucketPolicyArgs {
conditions: &conditions_fail,
..args
};
assert!(!policy.is_allowed(&args_fail).await, "Bucket Policy should deny non-matching IP");
}
}

View File

@@ -17,7 +17,7 @@ use super::compress::{CompressionConfig, CompressionPredicate};
use crate::admin;
use crate::auth::IAMAuth;
use crate::config;
use crate::server::{ReadinessGateLayer, ServiceState, ServiceStateManager, hybrid::hybrid, layer::RedirectLayer};
use crate::server::{ReadinessGateLayer, RemoteAddr, ServiceState, ServiceStateManager, hybrid::hybrid, layer::RedirectLayer};
use crate::storage;
use crate::storage::tonic_service::make_server;
use bytes::Bytes;
@@ -44,6 +44,7 @@ use tokio::net::{TcpListener, TcpStream};
use tokio_rustls::TlsAcceptor;
use tonic::{Request, Status, metadata::MetadataValue};
use tower::ServiceBuilder;
use tower_http::add_extension::AddExtensionLayer;
use tower_http::catch_panic::CatchPanicLayer;
use tower_http::compression::CompressionLayer;
use tower_http::cors::{AllowOrigin, Any, CorsLayer};
@@ -528,9 +529,21 @@ fn process_connection(
let rpc_service = NodeServiceServer::with_interceptor(make_server(), check_auth);
let service = hybrid(s3_service, rpc_service);
let remote_addr = match socket.peer_addr() {
Ok(addr) => Some(RemoteAddr(addr)),
Err(e) => {
tracing::warn!(
error = %e,
"Failed to obtain peer address; policy evaluation may fall back to a default source IP"
);
None
}
};
let hybrid_service = ServiceBuilder::new()
.layer(SetRequestIdLayer::x_request_id(MakeRequestUuid))
.layer(CatchPanicLayer::new())
.layer(AddExtensionLayer::new(remote_addr))
// CRITICAL: Insert ReadinessGateLayer before business logic
// This stops requests from hitting IAMAuth or Storage if they are not ready.
.layer(ReadinessGateLayer::new(readiness))

View File

@@ -36,3 +36,6 @@ pub(crate) use service_state::ServiceState;
pub(crate) use service_state::ServiceStateManager;
pub(crate) use service_state::ShutdownSignal;
pub(crate) use service_state::wait_for_shutdown;
#[derive(Clone, Copy, Debug)]
pub struct RemoteAddr(pub std::net::SocketAddr);

View File

@@ -15,6 +15,7 @@
use super::ecfs::FS;
use crate::auth::{check_key_valid, get_condition_values, get_session_token};
use crate::license::license_check;
use crate::server::RemoteAddr;
use rustfs_ecstore::bucket::policy_sys::PolicySys;
use rustfs_iam::error::Error as IamError;
use rustfs_policy::policy::action::{Action, S3Action};
@@ -36,6 +37,7 @@ pub(crate) struct ReqInfo {
/// Authorizes the request based on the action and credentials.
pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3Result<()> {
let remote_addr = req.extensions.get::<RemoteAddr>().map(|a| a.0);
let req_info = req.extensions.get_mut::<ReqInfo>().expect("ReqInfo not found");
if let Some(cred) = &req_info.cred {
@@ -48,7 +50,7 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
let default_claims = HashMap::new();
let claims = cred.claims.as_ref().unwrap_or(&default_claims);
let conditions = get_condition_values(&req.headers, cred, req_info.version_id.as_deref(), None);
let conditions = get_condition_values(&req.headers, cred, req_info.version_id.as_deref(), None, remote_addr);
if action == Action::S3Action(S3Action::DeleteObjectAction)
&& req_info.version_id.is_some()
@@ -109,6 +111,7 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
&rustfs_credentials::Credentials::default(),
req_info.version_id.as_deref(),
req.region.as_deref(),
remote_addr,
);
if action != Action::S3Action(S3Action::ListAllMyBucketsAction) {

View File

@@ -17,6 +17,7 @@ use crate::config::workload_profiles::{
RustFSBufferConfig, WorkloadProfile, get_global_buffer_config, is_buffer_profile_enabled,
};
use crate::error::ApiError;
use crate::server::RemoteAddr;
use crate::storage::concurrency::{
CachedGetObject, ConcurrencyManager, GetObjectGuard, get_concurrency_aware_buffer_size, get_concurrency_manager,
};
@@ -4689,7 +4690,8 @@ impl S3 for FS {
.await
.map_err(ApiError::from)?;
let conditions = get_condition_values(&req.headers, &rustfs_credentials::Credentials::default(), None, None);
let remote_addr = req.extensions.get::<RemoteAddr>().map(|a| a.0);
let conditions = get_condition_values(&req.headers, &rustfs_credentials::Credentials::default(), None, None, remote_addr);
let read_only = PolicySys::is_allowed(&BucketPolicyArgs {
bucket: &bucket,