mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-01 17:58:22 +00:00
fix(iam): stop stamping the wall clock on policy-less group reads (#6791)
This commit is contained in:
@@ -2320,9 +2320,13 @@ fn filter_policies_from_docs(policy_docs: &CacheEntity<PolicyDoc>, policy_name:
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_group_desc(name: &str, group_info: GroupInfo, mapped_policy: Option<MappedPolicy>) -> GroupDesc {
|
fn build_group_desc(name: &str, group_info: GroupInfo, mapped_policy: Option<MappedPolicy>) -> GroupDesc {
|
||||||
|
// A group without a mapped policy must report the group's own stored
|
||||||
|
// timestamp, not the wall clock: this value feeds content-addressed
|
||||||
|
// consumers (the site-replication repair plan hashes it), and a fresh
|
||||||
|
// clock on every read makes equal states hash differently.
|
||||||
let (policy, updated_at) = mapped_policy
|
let (policy, updated_at) = mapped_policy
|
||||||
.map(|policy| (policy.policies, Some(policy.update_at)))
|
.map(|policy| (policy.policies, Some(policy.update_at)))
|
||||||
.unwrap_or_else(|| (String::new(), Some(OffsetDateTime::now_utc())));
|
.unwrap_or_else(|| (String::new(), group_info.update_at));
|
||||||
|
|
||||||
GroupDesc {
|
GroupDesc {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
@@ -2347,6 +2351,46 @@ mod tests {
|
|||||||
};
|
};
|
||||||
use tokio::sync::Notify;
|
use tokio::sync::Notify;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_group_desc_without_mapping_reports_the_stored_group_timestamp() {
|
||||||
|
let stamp = OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("valid timestamp");
|
||||||
|
let group_info = GroupInfo {
|
||||||
|
version: 1,
|
||||||
|
status: "enabled".to_string(),
|
||||||
|
members: vec!["alice".to_string()],
|
||||||
|
update_at: Some(stamp),
|
||||||
|
};
|
||||||
|
|
||||||
|
let first = build_group_desc("team", group_info.clone(), None);
|
||||||
|
let second = build_group_desc("team", group_info, None);
|
||||||
|
|
||||||
|
assert_eq!(first.updated_at, Some(stamp));
|
||||||
|
assert_eq!(first.updated_at, second.updated_at);
|
||||||
|
assert!(first.policy.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_group_desc_with_mapping_reports_the_mapping_timestamp() {
|
||||||
|
let group_stamp = OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("valid timestamp");
|
||||||
|
let mapping_stamp = OffsetDateTime::from_unix_timestamp(1_700_000_500).expect("valid timestamp");
|
||||||
|
let group_info = GroupInfo {
|
||||||
|
version: 1,
|
||||||
|
status: "enabled".to_string(),
|
||||||
|
members: vec![],
|
||||||
|
update_at: Some(group_stamp),
|
||||||
|
};
|
||||||
|
let mapping = MappedPolicy {
|
||||||
|
version: 1,
|
||||||
|
policies: "readwrite".to_string(),
|
||||||
|
update_at: mapping_stamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
let desc = build_group_desc("team", group_info, Some(mapping));
|
||||||
|
|
||||||
|
assert_eq!(desc.updated_at, Some(mapping_stamp));
|
||||||
|
assert_eq!(desc.policy, "readwrite");
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct FailingInitialLoadStore;
|
struct FailingInitialLoadStore;
|
||||||
|
|
||||||
|
|||||||
@@ -1124,6 +1124,44 @@ fn test_site_replication_repair_dry_run_plan_is_non_mutating_and_redacted() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_site_replication_repair_preflight_token_is_deterministic_for_equal_state() {
|
||||||
|
// dry-run hands the operator a preflight token that execute must match, so
|
||||||
|
// equal state must hash equally across calls. A policy-less group used to
|
||||||
|
// break this: its desc was stamped with the wall clock on every read, and
|
||||||
|
// the flapping task id made execute permanently reject the token as stale.
|
||||||
|
let state = SiteReplicationState {
|
||||||
|
name: "local".to_string(),
|
||||||
|
service_account_access_key: "site-replicator-0".to_string(),
|
||||||
|
peers: BTreeMap::from([(
|
||||||
|
"remote-dep".to_string(),
|
||||||
|
PeerInfo {
|
||||||
|
deployment_id: "remote-dep".to_string(),
|
||||||
|
..peer("remote", "https://remote.example.com")
|
||||||
|
},
|
||||||
|
)]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut info = SRInfo::default();
|
||||||
|
info.group_desc_map.insert(
|
||||||
|
"policyless".to_string(),
|
||||||
|
rustfs_madmin::GroupDesc {
|
||||||
|
name: "policyless".to_string(),
|
||||||
|
status: "enabled".to_string(),
|
||||||
|
members: vec!["alice".to_string()],
|
||||||
|
policy: String::new(),
|
||||||
|
updated_at: Some(OffsetDateTime::UNIX_EPOCH),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let plan_a = site_replication_bootstrap_plan(&info).expect("first plan");
|
||||||
|
let plan_b = site_replication_bootstrap_plan(&info).expect("second plan");
|
||||||
|
let token_a = site_replication_repair_preflight_token(&state, &plan_a, b"test-signing-key").expect("first token");
|
||||||
|
let token_b = site_replication_repair_preflight_token(&state, &plan_b, b"test-signing-key").expect("second token");
|
||||||
|
|
||||||
|
assert_eq!(token_a, token_b);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_site_replication_repair_preflight_detects_stale_snapshot() {
|
fn test_site_replication_repair_preflight_detects_stale_snapshot() {
|
||||||
let mut state = SiteReplicationState {
|
let mut state = SiteReplicationState {
|
||||||
|
|||||||
Reference in New Issue
Block a user