mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-25 13:36:50 +00:00
Co-authored-by: 安正超 <anzhengchao@gmail.com>
This commit is contained in:
+253
-12
@@ -746,19 +746,29 @@ impl<T: Store> IamSys<T> {
|
|||||||
pub async fn is_allowed_sts(&self, args: &Args<'_>, parent_user: &str) -> bool {
|
pub async fn is_allowed_sts(&self, args: &Args<'_>, parent_user: &str) -> bool {
|
||||||
let is_owner = matches!(get_global_action_cred(), Some(cred) if cred.access_key == parent_user);
|
let is_owner = matches!(get_global_action_cred(), Some(cred) if cred.access_key == parent_user);
|
||||||
let role_arn = args.get_role_arn();
|
let role_arn = args.get_role_arn();
|
||||||
let policies = {
|
|
||||||
if is_owner {
|
|
||||||
Vec::new()
|
|
||||||
} else if role_arn.is_some() {
|
|
||||||
let Ok(arn) = ARN::parse(role_arn.unwrap_or_default()) else { return false };
|
|
||||||
|
|
||||||
MappedPolicy::new(self.roles_map.get(&arn).map_or_else(String::default, |v| v.clone()).as_str()).to_slice()
|
let (effective_groups, groups_source, policies) = if is_owner {
|
||||||
} else {
|
(None, "owner", Vec::new())
|
||||||
let Ok(p) = self.policy_db_get(parent_user, args.groups).await else { return false };
|
} else if let Some(arn_str) = role_arn {
|
||||||
|
let Ok(arn) = ARN::parse(arn_str) else { return false };
|
||||||
p
|
let p = MappedPolicy::new(self.roles_map.get(&arn).map_or_else(String::default, |v| v.clone()).as_str()).to_slice();
|
||||||
//TODO: FROM JWT
|
(None, "role", p)
|
||||||
}
|
} else {
|
||||||
|
let (effective_groups, groups_source) = match args.groups.as_ref() {
|
||||||
|
Some(g) if !g.is_empty() => (args.groups.clone(), "args"),
|
||||||
|
_ => match self.store.get_user(parent_user).await {
|
||||||
|
Some(u) => (u.credentials.groups.clone(), "parent_user_credentials"),
|
||||||
|
None => {
|
||||||
|
tracing::warn!(
|
||||||
|
parent_user = %parent_user,
|
||||||
|
"is_allowed_sts: groups fallback failed — parent user not found; policy evaluation will use no groups"
|
||||||
|
);
|
||||||
|
(None, "parent_user_credentials")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let Ok(p) = self.policy_db_get(parent_user, &effective_groups).await else { return false };
|
||||||
|
(effective_groups, groups_source, p)
|
||||||
};
|
};
|
||||||
|
|
||||||
if !is_owner && policies.is_empty() {
|
if !is_owner && policies.is_empty() {
|
||||||
@@ -778,6 +788,16 @@ impl<T: Store> IamSys<T> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let (has_session_policy, is_allowed_sp) = is_allowed_by_session_policy(args);
|
let (has_session_policy, is_allowed_sp) = is_allowed_by_session_policy(args);
|
||||||
|
tracing::debug!(
|
||||||
|
"is_allowed_sts: action={:?}, has_session_policy={}, is_allowed_sp={}, is_owner={}, parent_user={}, groups_source={}, effective_groups={:?}",
|
||||||
|
args.action,
|
||||||
|
has_session_policy,
|
||||||
|
is_allowed_sp,
|
||||||
|
is_owner,
|
||||||
|
parent_user,
|
||||||
|
groups_source,
|
||||||
|
effective_groups
|
||||||
|
);
|
||||||
if has_session_policy {
|
if has_session_policy {
|
||||||
return is_allowed_sp && (is_owner || combined_policy.is_allowed(args).await);
|
return is_allowed_sp && (is_owner || combined_policy.is_allowed(args).await);
|
||||||
}
|
}
|
||||||
@@ -978,3 +998,224 @@ pub fn get_claims_from_token_with_secret(token: &str, secret: &str) -> Result<Ha
|
|||||||
}
|
}
|
||||||
Ok(ms.claims)
|
Ok(ms.claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cache::{Cache, CacheEntity};
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::manager::get_default_policyes;
|
||||||
|
use crate::store::{GroupInfo, MappedPolicy, Store, UserType};
|
||||||
|
use rustfs_credentials::Credentials;
|
||||||
|
use rustfs_policy::auth::UserIdentity;
|
||||||
|
use rustfs_policy::policy::Args;
|
||||||
|
use rustfs_policy::policy::action::{Action, S3Action};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
/// Mock Store that populates cache in load_all so STS temp credentials
|
||||||
|
/// without args.groups still get group-attached policies via parent user (groups fallback).
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct StsGroupsFallbackMockStore;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Store for StsGroupsFallbackMockStore {
|
||||||
|
fn has_watcher(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_iam_config<Item: serde::Serialize + Send>(&self, _item: Item, _path: impl AsRef<str> + Send) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_iam_config<Item: serde::de::DeserializeOwned>(&self, _path: impl AsRef<str> + Send) -> Result<Item> {
|
||||||
|
Err(Error::ConfigNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_iam_config(&self, _path: impl AsRef<str> + Send) -> Result<()> {
|
||||||
|
Err(Error::InvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_user_identity(
|
||||||
|
&self,
|
||||||
|
_name: &str,
|
||||||
|
_user_type: UserType,
|
||||||
|
_item: UserIdentity,
|
||||||
|
_ttl: Option<usize>,
|
||||||
|
) -> Result<()> {
|
||||||
|
Err(Error::InvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_user_identity(&self, _name: &str, _user_type: UserType) -> Result<()> {
|
||||||
|
Err(Error::InvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_user_identity(&self, _name: &str, _user_type: UserType) -> Result<UserIdentity> {
|
||||||
|
Err(Error::InvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_user(&self, _name: &str, _user_type: UserType, _m: &mut HashMap<String, UserIdentity>) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_users(&self, _user_type: UserType, _m: &mut HashMap<String, UserIdentity>) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_secret_key(&self, _name: &str, _user_type: UserType) -> Result<String> {
|
||||||
|
Err(Error::InvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_group_info(&self, _name: &str, _item: GroupInfo) -> Result<()> {
|
||||||
|
Err(Error::InvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_group_info(&self, _name: &str) -> Result<()> {
|
||||||
|
Err(Error::InvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_group(&self, _name: &str, _m: &mut HashMap<String, GroupInfo>) -> Result<()> {
|
||||||
|
Err(Error::InvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_groups(&self, _m: &mut HashMap<String, GroupInfo>) -> Result<()> {
|
||||||
|
Err(Error::InvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_policy_doc(&self, _name: &str, _item: rustfs_policy::policy::PolicyDoc) -> Result<()> {
|
||||||
|
Err(Error::InvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_policy_doc(&self, _name: &str) -> Result<()> {
|
||||||
|
Err(Error::InvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_policy(&self, _name: &str) -> Result<rustfs_policy::policy::PolicyDoc> {
|
||||||
|
Err(Error::InvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_policy_doc(&self, _name: &str, _m: &mut HashMap<String, rustfs_policy::policy::PolicyDoc>) -> Result<()> {
|
||||||
|
Err(Error::InvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_policy_docs(&self, _m: &mut HashMap<String, rustfs_policy::policy::PolicyDoc>) -> Result<()> {
|
||||||
|
Err(Error::InvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_mapped_policy(
|
||||||
|
&self,
|
||||||
|
_name: &str,
|
||||||
|
_user_type: UserType,
|
||||||
|
_is_group: bool,
|
||||||
|
_item: MappedPolicy,
|
||||||
|
_ttl: Option<usize>,
|
||||||
|
) -> Result<()> {
|
||||||
|
Err(Error::InvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_mapped_policy(&self, _name: &str, _user_type: UserType, _is_group: bool) -> Result<()> {
|
||||||
|
Err(Error::InvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_mapped_policy(
|
||||||
|
&self,
|
||||||
|
_name: &str,
|
||||||
|
_user_type: UserType,
|
||||||
|
_is_group: bool,
|
||||||
|
_m: &mut HashMap<String, MappedPolicy>,
|
||||||
|
) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_mapped_policies(
|
||||||
|
&self,
|
||||||
|
_user_type: UserType,
|
||||||
|
_is_group: bool,
|
||||||
|
_m: &mut HashMap<String, MappedPolicy>,
|
||||||
|
) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_all(&self, cache: &Cache) -> Result<()> {
|
||||||
|
const PARENT_USER: &str = "sts-fallback-test-parent";
|
||||||
|
const GROUP_NAME: &str = "testgroup";
|
||||||
|
|
||||||
|
let policy_docs = get_default_policyes();
|
||||||
|
cache
|
||||||
|
.policy_docs
|
||||||
|
.store(Arc::new(CacheEntity::new(policy_docs).update_load_time()));
|
||||||
|
|
||||||
|
let creds = Credentials {
|
||||||
|
access_key: PARENT_USER.to_string(),
|
||||||
|
secret_key: "longenoughsecret".to_string(),
|
||||||
|
session_token: String::new(),
|
||||||
|
expiration: None,
|
||||||
|
status: "on".to_string(),
|
||||||
|
parent_user: String::new(),
|
||||||
|
groups: Some(vec![GROUP_NAME.to_string()]),
|
||||||
|
claims: None,
|
||||||
|
name: None,
|
||||||
|
description: None,
|
||||||
|
};
|
||||||
|
let parent_identity = UserIdentity {
|
||||||
|
version: 1,
|
||||||
|
credentials: creds,
|
||||||
|
update_at: Some(OffsetDateTime::now_utc()),
|
||||||
|
};
|
||||||
|
let mut users = HashMap::new();
|
||||||
|
users.insert(PARENT_USER.to_string(), parent_identity);
|
||||||
|
cache.users.store(Arc::new(CacheEntity::new(users).update_load_time()));
|
||||||
|
|
||||||
|
let group = GroupInfo::new(vec![PARENT_USER.to_string()]);
|
||||||
|
let mut groups = HashMap::new();
|
||||||
|
groups.insert(GROUP_NAME.to_string(), group);
|
||||||
|
cache.groups.store(Arc::new(CacheEntity::new(groups).update_load_time()));
|
||||||
|
|
||||||
|
let group_policy = MappedPolicy::new("readwrite");
|
||||||
|
let mut group_policies = HashMap::new();
|
||||||
|
group_policies.insert(GROUP_NAME.to_string(), group_policy);
|
||||||
|
cache
|
||||||
|
.group_policies
|
||||||
|
.store(Arc::new(CacheEntity::new(group_policies).update_load_time()));
|
||||||
|
|
||||||
|
cache.user_policies.store(Arc::new(CacheEntity::default().update_load_time()));
|
||||||
|
cache.sts_accounts.store(Arc::new(CacheEntity::default().update_load_time()));
|
||||||
|
cache.sts_policies.store(Arc::new(CacheEntity::default().update_load_time()));
|
||||||
|
cache.build_user_group_memberships();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Regression test: temp credentials without groups in args still receive group-attached
|
||||||
|
/// policies via the parent user (groups fallback). Without the fallback, policy_db_get
|
||||||
|
/// would get None for groups and the user would have no group policies, so the action
|
||||||
|
/// would be denied.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_sts_groups_fallback_temp_creds_receive_parent_group_policies() {
|
||||||
|
let store = StsGroupsFallbackMockStore;
|
||||||
|
let cache_manager = IamCache::new(store).await;
|
||||||
|
let iam_sys = IamSys::new(cache_manager);
|
||||||
|
|
||||||
|
let parent_user = "sts-fallback-test-parent";
|
||||||
|
let claims = HashMap::new();
|
||||||
|
let groups: Option<Vec<String>> = None;
|
||||||
|
let args = Args {
|
||||||
|
account: parent_user,
|
||||||
|
groups: &groups,
|
||||||
|
action: Action::S3Action(S3Action::ListBucketAction),
|
||||||
|
bucket: "mybucket",
|
||||||
|
conditions: &HashMap::new(),
|
||||||
|
is_owner: false,
|
||||||
|
object: "",
|
||||||
|
claims: &claims,
|
||||||
|
deny_only: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let allowed = iam_sys.is_allowed_sts(&args, parent_user).await;
|
||||||
|
assert!(
|
||||||
|
allowed,
|
||||||
|
"STS temp credentials with no groups in args should still be allowed via parent user's group policy (readwrite)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user