mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-10 07:06:53 +00:00
fix(sts): align AssumeRole authorization (#5281)
* fix(sts): align AssumeRole authorization with MinIO * test(sts): cover AssumeRole OPA contract * fix(iam): fail closed on unresolved policies * fix(iam): fail closed while OPA initializes --------- Co-authored-by: cxymds <cxymds@gmail.com> Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
This commit is contained in:
+233
-30
@@ -41,7 +41,7 @@ use rustfs_policy::policy::Args;
|
||||
use rustfs_policy::policy::opa;
|
||||
use rustfs_policy::policy::{Policy, PolicyDoc, iam_policy_claim_name_sa, policy_needs_existing_object_tag_for_args};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use time::OffsetDateTime;
|
||||
@@ -61,10 +61,45 @@ const VERIFIED_FEDERATED_POLICY_CLAIM: &str = "x-rustfs-internal-federated-polic
|
||||
const FEDERATED_POLICY_BOUNDARY_CLAIM: &str = "x-rustfs-internal-federated-boundary";
|
||||
pub(crate) const SITE_REPLICATOR_CLAIM: &str = "site-replicator";
|
||||
|
||||
static POLICY_PLUGIN_CLIENT: OnceLock<Arc<RwLock<Option<rustfs_policy::policy::opa::AuthZPlugin>>>> = OnceLock::new();
|
||||
#[derive(Clone)]
|
||||
enum PolicyPluginState {
|
||||
Disabled,
|
||||
Initializing,
|
||||
Ready(opa::AuthZPlugin),
|
||||
Failed,
|
||||
}
|
||||
|
||||
fn get_policy_plugin_client() -> Arc<RwLock<Option<rustfs_policy::policy::opa::AuthZPlugin>>> {
|
||||
POLICY_PLUGIN_CLIENT.get_or_init(|| Arc::new(RwLock::new(None))).clone()
|
||||
static POLICY_PLUGIN_STATE: OnceLock<Arc<RwLock<PolicyPluginState>>> = OnceLock::new();
|
||||
|
||||
fn get_policy_plugin_state() -> Arc<RwLock<PolicyPluginState>> {
|
||||
POLICY_PLUGIN_STATE
|
||||
.get_or_init(|| {
|
||||
let configured = opa::is_configured();
|
||||
let state = Arc::new(RwLock::new(if configured {
|
||||
PolicyPluginState::Initializing
|
||||
} else {
|
||||
PolicyPluginState::Disabled
|
||||
}));
|
||||
if configured {
|
||||
let state = Arc::clone(&state);
|
||||
tokio::spawn(async move {
|
||||
let next_state = match opa::lookup_config().await {
|
||||
Ok(conf) if conf.enable() => {
|
||||
info!("OPA plugin enabled");
|
||||
PolicyPluginState::Ready(opa::AuthZPlugin::new(conf))
|
||||
}
|
||||
Ok(_) => PolicyPluginState::Failed,
|
||||
Err(e) => {
|
||||
error!("Error loading OPA configuration err:{}", e);
|
||||
PolicyPluginState::Failed
|
||||
}
|
||||
};
|
||||
*state.write().await = next_state;
|
||||
});
|
||||
}
|
||||
state
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub struct IamSys<T> {
|
||||
@@ -173,19 +208,7 @@ impl<T: Store> IamSys<T> {
|
||||
/// # Returns
|
||||
/// A new instance of IamSys
|
||||
pub fn new(store: Arc<IamCache<T>>) -> Self {
|
||||
tokio::spawn(async move {
|
||||
match opa::lookup_config().await {
|
||||
Ok(conf) => {
|
||||
if conf.enable() {
|
||||
Self::set_policy_plugin_client(opa::AuthZPlugin::new(conf)).await;
|
||||
info!("OPA plugin enabled");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error loading OPA configuration err:{}", e);
|
||||
}
|
||||
};
|
||||
});
|
||||
get_policy_plugin_state();
|
||||
|
||||
Self {
|
||||
store,
|
||||
@@ -206,15 +229,22 @@ impl<T: Store> IamSys<T> {
|
||||
}
|
||||
|
||||
pub async fn set_policy_plugin_client(client: rustfs_policy::policy::opa::AuthZPlugin) {
|
||||
let policy_plugin_client = get_policy_plugin_client();
|
||||
let mut guard = policy_plugin_client.write().await;
|
||||
*guard = Some(client);
|
||||
let policy_plugin_state = get_policy_plugin_state();
|
||||
let mut guard = policy_plugin_state.write().await;
|
||||
*guard = PolicyPluginState::Ready(client);
|
||||
}
|
||||
|
||||
pub async fn get_policy_plugin_client() -> Option<rustfs_policy::policy::opa::AuthZPlugin> {
|
||||
let policy_plugin_client = get_policy_plugin_client();
|
||||
let guard = policy_plugin_client.read().await;
|
||||
guard.clone()
|
||||
let policy_plugin_state = get_policy_plugin_state();
|
||||
let guard = policy_plugin_state.read().await;
|
||||
match &*guard {
|
||||
PolicyPluginState::Ready(client) => Some(client.clone()),
|
||||
PolicyPluginState::Disabled | PolicyPluginState::Initializing | PolicyPluginState::Failed => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn policy_plugin_state() -> PolicyPluginState {
|
||||
get_policy_plugin_state().read().await.clone()
|
||||
}
|
||||
|
||||
pub async fn load_group(&self, name: &str) -> Result<()> {
|
||||
@@ -1060,11 +1090,20 @@ impl<T: Store> IamSys<T> {
|
||||
};
|
||||
}
|
||||
|
||||
if Self::get_policy_plugin_client().await.is_some() {
|
||||
return PreparedIamAuth {
|
||||
needs_existing_object_tag: false,
|
||||
mode: PreparedIamMode::Opa,
|
||||
};
|
||||
match Self::policy_plugin_state().await {
|
||||
PolicyPluginState::Ready(_) => {
|
||||
return PreparedIamAuth {
|
||||
needs_existing_object_tag: false,
|
||||
mode: PreparedIamMode::Opa,
|
||||
};
|
||||
}
|
||||
PolicyPluginState::Initializing | PolicyPluginState::Failed => {
|
||||
return PreparedIamAuth {
|
||||
needs_existing_object_tag: false,
|
||||
mode: PreparedIamMode::Deny,
|
||||
};
|
||||
}
|
||||
PolicyPluginState::Disabled => {}
|
||||
}
|
||||
|
||||
let Ok((is_svc, parent_user)) = self.is_service_account(args.account).await else {
|
||||
@@ -1163,7 +1202,22 @@ impl<T: Store> IamSys<T> {
|
||||
};
|
||||
}
|
||||
|
||||
let combined_policy = self.get_combined_policy(&policies).await;
|
||||
let (resolved_policies, combined_policy) = self.store.merge_policies(&policies.join(",")).await;
|
||||
if args.deny_only {
|
||||
let resolved_policy_names: HashSet<&str> = resolved_policies
|
||||
.split(',')
|
||||
.filter(|policy_name| !policy_name.trim().is_empty())
|
||||
.collect();
|
||||
if policies
|
||||
.iter()
|
||||
.any(|policy_name| !resolved_policy_names.contains(policy_name.as_str()))
|
||||
{
|
||||
return PreparedIamAuth {
|
||||
needs_existing_object_tag: false,
|
||||
mode: PreparedIamMode::Deny,
|
||||
};
|
||||
}
|
||||
}
|
||||
PreparedIamAuth {
|
||||
needs_existing_object_tag: policy_needs_existing_object_tag_for_args(&combined_policy, args).await,
|
||||
mode: PreparedIamMode::Regular { combined_policy },
|
||||
@@ -1703,7 +1757,7 @@ mod tests {
|
||||
fn deprecated_list_polices_api_is_available() {
|
||||
let _ = IamSys::<StsTestMockStore>::list_polices;
|
||||
}
|
||||
use rustfs_policy::policy::action::{Action, AdminAction, S3Action};
|
||||
use rustfs_policy::policy::action::{Action, AdminAction, S3Action, StsAction};
|
||||
use rustfs_policy::policy::policy_uses_existing_object_tag_conditions;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
@@ -3309,6 +3363,155 @@ mod tests {
|
||||
assert!(!iam_sys.eval_prepared(&prepared, &args).await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn regular_deny_only_rejects_unresolved_mapped_policy() {
|
||||
let iam_sys = test_iam_sys().await;
|
||||
let user = "regular-unresolved-policy";
|
||||
let identity = UserIdentity::from(Credentials {
|
||||
access_key: user.to_string(),
|
||||
secret_key: "longenoughsecret".to_string(),
|
||||
status: ACCOUNT_ON.to_string(),
|
||||
..Default::default()
|
||||
});
|
||||
iam_sys.store.cache.with_write_lock(|cache| {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
cache.add_or_update_user(user, &identity, now);
|
||||
cache.add_or_update_user_policy(user, &MappedPolicy::new("readwrite,missing-policy"), now);
|
||||
});
|
||||
|
||||
let groups = None;
|
||||
let claims = HashMap::new();
|
||||
let conditions = HashMap::new();
|
||||
let args = Args {
|
||||
account: user,
|
||||
groups: &groups,
|
||||
action: Action::StsAction(StsAction::AssumeRoleAction),
|
||||
bucket: "",
|
||||
conditions: &conditions,
|
||||
is_owner: false,
|
||||
object: "",
|
||||
claims: &claims,
|
||||
deny_only: true,
|
||||
};
|
||||
|
||||
let prepared = iam_sys.prepare_regular_auth(&args).await;
|
||||
assert!(matches!(prepared.mode, PreparedIamMode::Deny));
|
||||
assert!(
|
||||
!iam_sys.eval_prepared(&prepared, &args).await,
|
||||
"deny-only evaluation must fail closed when a mapped policy document is unresolved"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn regular_full_evaluation_keeps_resolved_part_of_partial_mapping() {
|
||||
let iam_sys = test_iam_sys().await;
|
||||
let user = "regular-partial-policy";
|
||||
let identity = UserIdentity::from(Credentials {
|
||||
access_key: user.to_string(),
|
||||
secret_key: "longenoughsecret".to_string(),
|
||||
status: ACCOUNT_ON.to_string(),
|
||||
..Default::default()
|
||||
});
|
||||
iam_sys.store.cache.with_write_lock(|cache| {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
cache.add_or_update_user(user, &identity, now);
|
||||
cache.add_or_update_user_policy(user, &MappedPolicy::new("readwrite,missing-policy"), now);
|
||||
});
|
||||
|
||||
let groups = None;
|
||||
let claims = HashMap::new();
|
||||
let conditions = HashMap::new();
|
||||
let args = Args {
|
||||
account: user,
|
||||
groups: &groups,
|
||||
action: Action::S3Action(S3Action::GetObjectAction),
|
||||
bucket: "bucket",
|
||||
conditions: &conditions,
|
||||
is_owner: false,
|
||||
object: "object",
|
||||
claims: &claims,
|
||||
deny_only: false,
|
||||
};
|
||||
|
||||
let prepared = iam_sys.prepare_regular_auth(&args).await;
|
||||
assert!(matches!(prepared.mode, PreparedIamMode::Regular { .. }));
|
||||
assert!(
|
||||
iam_sys.eval_prepared(&prepared, &args).await,
|
||||
"full evaluation must preserve the historical partial-resolution behavior"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn regular_deny_only_honors_group_derived_explicit_deny() {
|
||||
let iam_sys = test_iam_sys().await;
|
||||
let deny_policy =
|
||||
Policy::parse_config(br#"{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":["sts:AssumeRole"]}]}"#)
|
||||
.expect("group deny policy should parse");
|
||||
let now = OffsetDateTime::now_utc();
|
||||
iam_sys
|
||||
.store
|
||||
.cache
|
||||
.add_or_update_policy_doc("deny-assume-role", &PolicyDoc::new(deny_policy), now);
|
||||
iam_sys
|
||||
.store
|
||||
.cache
|
||||
.add_or_update_group_policy("testgroup", &MappedPolicy::new("deny-assume-role"), now);
|
||||
|
||||
let groups = Some(vec!["testgroup".to_string()]);
|
||||
let claims = HashMap::new();
|
||||
let conditions = HashMap::new();
|
||||
let args = Args {
|
||||
account: "sts-fallback-test-parent",
|
||||
groups: &groups,
|
||||
action: Action::StsAction(StsAction::AssumeRoleAction),
|
||||
bucket: "",
|
||||
conditions: &conditions,
|
||||
is_owner: false,
|
||||
object: "",
|
||||
claims: &claims,
|
||||
deny_only: true,
|
||||
};
|
||||
|
||||
let prepared = iam_sys.prepare_regular_auth(&args).await;
|
||||
assert!(matches!(prepared.mode, PreparedIamMode::Regular { .. }));
|
||||
assert!(
|
||||
!iam_sys.eval_prepared(&prepared, &args).await,
|
||||
"group-derived explicit Deny must remain authoritative"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn regular_deny_only_rejects_unresolved_group_mapping() {
|
||||
let iam_sys = test_iam_sys().await;
|
||||
iam_sys.store.cache.add_or_update_group_policy(
|
||||
"testgroup",
|
||||
&MappedPolicy::new("readwrite,missing-policy"),
|
||||
OffsetDateTime::now_utc(),
|
||||
);
|
||||
|
||||
let groups = Some(vec!["testgroup".to_string()]);
|
||||
let claims = HashMap::new();
|
||||
let conditions = HashMap::new();
|
||||
let args = Args {
|
||||
account: "sts-fallback-test-parent",
|
||||
groups: &groups,
|
||||
action: Action::StsAction(StsAction::AssumeRoleAction),
|
||||
bucket: "",
|
||||
conditions: &conditions,
|
||||
is_owner: false,
|
||||
object: "",
|
||||
claims: &claims,
|
||||
deny_only: true,
|
||||
};
|
||||
|
||||
let prepared = iam_sys.prepare_regular_auth(&args).await;
|
||||
assert!(matches!(prepared.mode, PreparedIamMode::Deny));
|
||||
assert!(
|
||||
!iam_sys.eval_prepared(&prepared, &args).await,
|
||||
"deny-only evaluation must fail closed for an unresolved group-derived mapping"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sts_claim_policy_ignores_unsafe_and_missing_policy_names() {
|
||||
let store = StsTestMockStore::new(true);
|
||||
|
||||
Reference in New Issue
Block a user