fix(site-replication): persist source stamps and apply IAM items atomically (backlog#2291)

Review findings on rustfs#7195: the receive-side staleness gate compared a
source `updatedAt` against a stamp the local write had put on the record,
and the verdict, the write and the deletion mark were three separate steps.

- IAM writes gain explicit-stamp variants (`set_policy_at`, `policy_db_set_at`,
  group and user `*_at`, `new_service_account_at`, `update_service_account_at`)
  so a replicated record carries its source time; local edits are unchanged.
- `apply_iam_item` runs verdict, write and mark commit under the
  site-replication state transaction (distributed state-object lock), so a
  concurrent older grant and newer revoke are ordered on every node.
- A replicated service account is created with its source status in one
  write (`NewServiceAccountOpts::status`), never enabled transiently.
- Deletion marks are pruned by age (30 days) instead of by count.
- Bucket-config deletes persist the source stamp (`delete_if_incarnation_at`).

Regressions run through the real receiver: delayed in-order updates for every
gated item type, concurrent grant/revoke, delete then stale re-create, disabled
service-account create, delete stamping in ecstore, and mark retention.
This commit is contained in:
唐小鸭
2026-09-06 03:05:10 +08:00
parent 810ebb44fc
commit 10575e851a
10 changed files with 1048 additions and 222 deletions
+10 -10
View File
@@ -199,16 +199,16 @@ pub mod bucket {
pub use crate::bucket::metadata_sys::{
BucketMetadataMutationGuard, BucketMetadataSys, ObjectLockConfigState, acquire_bucket_metadata_transaction_lock,
acquire_bucket_metadata_transaction_lock_for_incarnation, acquire_scanner_bucket_incarnation_fence,
capture_bucket_metadata_incarnation, delete, delete_if_incarnation, delete_under_transaction_lock, get,
get_accelerate_config, get_bucket_policy, get_bucket_policy_raw, get_bucket_targets_config, get_config_from_disk,
get_cors_config, get_durability_config, get_global_bucket_metadata_sys, get_lifecycle_config, get_logging_config,
get_notification_config, get_object_lock_config, get_object_lock_config_state, get_on_demand_migration_config,
get_public_access_block_config, get_quota_config, get_replication_config, get_request_payment_config, get_sse_config,
get_tagging_config, get_versioning_config, get_website_config, init_bucket_metadata_sys, list_bucket_targets,
reload_bucket_metadata, remove_bucket_metadata, set_bucket_metadata, update,
update_bucket_targets_under_transaction_lock, update_config_with, update_if_incarnation, update_if_incarnation_at,
update_quota_if_incarnation, update_quota_if_incarnation_at, update_under_transaction_lock,
update_under_transaction_lock_at,
capture_bucket_metadata_incarnation, delete, delete_if_incarnation, delete_if_incarnation_at,
delete_under_transaction_lock, get, get_accelerate_config, get_bucket_policy, get_bucket_policy_raw,
get_bucket_targets_config, get_config_from_disk, get_cors_config, get_durability_config,
get_global_bucket_metadata_sys, get_lifecycle_config, get_logging_config, get_notification_config,
get_object_lock_config, get_object_lock_config_state, get_on_demand_migration_config, get_public_access_block_config,
get_quota_config, get_replication_config, get_request_payment_config, get_sse_config, get_tagging_config,
get_versioning_config, get_website_config, init_bucket_metadata_sys, list_bucket_targets, reload_bucket_metadata,
remove_bucket_metadata, set_bucket_metadata, update, update_bucket_targets_under_transaction_lock,
update_config_with, update_if_incarnation, update_if_incarnation_at, update_quota_if_incarnation,
update_quota_if_incarnation_at, update_under_transaction_lock, update_under_transaction_lock_at,
};
}
+81 -4
View File
@@ -617,6 +617,30 @@ pub async fn delete_if_incarnation(bucket: &str, config_file: &str, expected_inc
bucket,
config_file,
Some(expected_incarnation_id),
None,
))
.await
}
/// [`delete_if_incarnation`] stamping the cleared config with `updated_at`
/// (a replicated deletion's source time) instead of the local clock.
///
/// The stamp survives the deletion as the config's `*_config_updated_at`, and
/// that is what the next incoming item is judged against: a local stamp on
/// the delete would reject a newer source re-create that was merely delivered
/// later (backlog#2292). See [`update_if_incarnation_at`].
pub async fn delete_if_incarnation_at(
bucket: &str,
config_file: &str,
expected_incarnation_id: Uuid,
updated_at: OffsetDateTime,
) -> Result<OffsetDateTime> {
Box::pin(delete_with_sys_expected(
get_bucket_metadata_sys()?,
bucket,
config_file,
Some(expected_incarnation_id),
Some(updated_at),
))
.await
}
@@ -659,17 +683,20 @@ async fn update_with_sys_expected(
/// [`delete`] against an explicitly supplied metadata system. See
/// [`update_with_sys`].
async fn delete_with_sys(sys: Arc<RwLock<BucketMetadataSys>>, bucket: &str, config_file: &str) -> Result<OffsetDateTime> {
delete_with_sys_expected(sys, bucket, config_file, None).await
delete_with_sys_expected(sys, bucket, config_file, None, None).await
}
/// `updated_at`: `None` stamps the local clock; `Some` persists a replicated
/// deletion's source time (backlog#2292).
async fn delete_with_sys_expected(
sys: Arc<RwLock<BucketMetadataSys>>,
bucket: &str,
config_file: &str,
expected_incarnation_id: Option<Uuid>,
updated_at: Option<OffsetDateTime>,
) -> Result<OffsetDateTime> {
let guard = acquire_config_write_guard_for_incarnation(sys.clone(), bucket, expected_incarnation_id).await?;
delete_under_config_write_guard(sys, &guard, config_file).await
delete_under_config_write_guard(sys, &guard, config_file, updated_at).await
}
/// Owns the complete bucket-config mutation fence.
@@ -840,7 +867,7 @@ pub async fn delete_under_transaction_lock(
config_file: &str,
) -> Result<OffsetDateTime> {
guard.ensure_valid(bucket)?;
delete_under_config_write_guard(get_bucket_metadata_sys()?, guard, config_file).await
delete_under_config_write_guard(get_bucket_metadata_sys()?, guard, config_file, None).await
}
pub async fn update_quota_if_incarnation(
@@ -928,6 +955,7 @@ async fn delete_under_config_write_guard(
sys: Arc<RwLock<BucketMetadataSys>>,
guard: &BucketMetadataMutationGuard,
config_file: &str,
updated_at: Option<OffsetDateTime>,
) -> Result<OffsetDateTime> {
guard.ensure_valid(&guard.bucket)?;
let metadata_sys = sys.read().await.clone();
@@ -939,7 +967,7 @@ async fn delete_under_config_write_guard(
Some(&guard.transaction_guard),
&guard.bucket,
"bucket config deletion transaction",
metadata_sys.update_checked(&guard.bucket, config_file, Vec::new(), false, guard.incarnation_id, None),
metadata_sys.update_checked(&guard.bucket, config_file, Vec::new(), false, guard.incarnation_id, updated_at),
),
)
.await?;
@@ -3890,6 +3918,55 @@ mod tests {
);
}
/// backlog#2292: a replicated delete persists the source time as the
/// cleared config's `*_config_updated_at`, so the receive-side gate
/// (source time against stored stamp) lets a newer source re-create land
/// even when the delete was applied later than the re-create's source
/// time; the plain delete keeps stamping the local clock.
#[tokio::test]
async fn explicit_updated_at_is_persisted_by_a_delete() {
let (dirs, ecstore) = isolated_store_over_temp_disks().await;
let bucket = "source-stamped-delete";
for dir in &dirs {
std::fs::create_dir_all(dir.path().join(bucket)).expect("bucket volume should be created");
}
let sys = Arc::new(RwLock::new(BucketMetadataSys::new(ecstore)));
let policy = br#"{"Version":"2012-10-17","Statement":[]}"#.to_vec();
let created_at = OffsetDateTime::now_utc() - Duration::from_secs(3 * 3600);
let deleted_at = created_at + Duration::from_secs(60);
let recreated_at = deleted_at + Duration::from_secs(60);
update_with_sys_expected(sys.clone(), bucket, BUCKET_POLICY_CONFIG, policy.clone(), None, Some(created_at))
.await
.expect("source-stamped policy write should persist");
let stamped = delete_with_sys_expected(sys.clone(), bucket, BUCKET_POLICY_CONFIG, None, Some(deleted_at))
.await
.expect("source-stamped policy delete should persist");
assert_eq!(stamped, deleted_at);
let metadata_sys = sys.read().await.clone();
metadata_sys.metadata_map.write().await.clear();
let reloaded = metadata_sys.get_config_from_disk(bucket).await.expect("reload from disk");
assert!(reloaded.policy_config_json.is_empty(), "the delete cleared the payload");
assert_eq!(reloaded.policy_config_updated_at, deleted_at, "the delete kept the source stamp");
assert!(
recreated_at >= reloaded.policy_config_updated_at,
"a re-create newer than the delete's source time is not stale against the stored stamp"
);
// The plain delete path is unchanged: stamped with the local clock.
update_with_sys_expected(sys.clone(), bucket, BUCKET_POLICY_CONFIG, policy, None, Some(recreated_at))
.await
.expect("re-create should persist");
let before = OffsetDateTime::now_utc();
let stamped = delete_with_sys_expected(sys.clone(), bucket, BUCKET_POLICY_CONFIG, None, None)
.await
.expect("locally stamped delete should persist");
assert!(stamped >= before, "the plain delete path must keep stamping the local clock");
let reloaded = metadata_sys.get_config_from_disk(bucket).await.expect("reload from disk");
assert_eq!(reloaded.policy_config_updated_at, stamped);
}
/// The load and the persisted write share one write guard, so concurrent
/// rewrites of the same config compose instead of clobbering each other.
/// Moving the load outside that guard loses all but the last tag.
+154 -32
View File
@@ -555,6 +555,17 @@ where
}
pub async fn set_policy(&self, name: &str, policy: Policy) -> Result<OffsetDateTime> {
self.set_policy_at(name, policy, OffsetDateTime::now_utc()).await
}
/// [`Self::set_policy`] stamping the document with `updated_at` instead
/// of the local clock.
///
/// A site-replication receiver passes the edit's source time: the next
/// incoming revision is judged against the stored `UpdateDate`, so a
/// local stamp would reject a newer source edit that was merely delivered
/// later (backlog#2291). The returned stamp is the one persisted.
pub async fn set_policy_at(&self, name: &str, policy: Policy, updated_at: OffsetDateTime) -> Result<OffsetDateTime> {
if name.is_empty() || policy.is_empty() {
return Err(Error::InvalidArgument);
}
@@ -565,18 +576,17 @@ where
.get(name)
.map(|v| {
let mut p = v.clone();
p.update(policy.clone());
p.update_at(policy.clone(), updated_at);
p
})
.unwrap_or_else(|| PolicyDoc::new(policy));
.unwrap_or_else(|| PolicyDoc::new_at(policy, updated_at));
self.api.save_policy_doc(name, policy_doc.clone()).await?;
let now = OffsetDateTime::now_utc();
self.cache
.add_or_update_policy_doc(name, &policy_doc, OffsetDateTime::now_utc());
self.cache.add_or_update_policy_doc(name, &policy_doc, now);
Ok(now)
Ok(updated_at)
}
pub async fn list_policies(&self, bucket_name: &str) -> Result<HashMap<String, Policy>> {
@@ -810,6 +820,12 @@ where
/// create a service account and update cache
pub async fn add_service_account(&self, cred: Credentials) -> Result<OffsetDateTime> {
self.add_service_account_at(cred, OffsetDateTime::now_utc()).await
}
/// [`Self::add_service_account`] stamping the identity with `updated_at`
/// instead of the local clock; see [`Self::set_policy_at`] (backlog#2291).
pub async fn add_service_account_at(&self, cred: Credentials, updated_at: OffsetDateTime) -> Result<OffsetDateTime> {
if cred.access_key.is_empty() || cred.parent_user.is_empty() {
return Err(Error::InvalidArgument);
}
@@ -821,7 +837,8 @@ where
}
drop(cache);
let u = UserIdentity::new(cred);
let mut u = UserIdentity::new(cred);
u.update_at = Some(updated_at);
self.api
.save_user_identity(&u.credentials.access_key, UserType::Svc, u.clone(), None)
@@ -829,10 +846,22 @@ where
self.update_user_with_claims(&u.credentials.access_key, u.clone())?;
Ok(OffsetDateTime::now_utc())
Ok(updated_at)
}
pub async fn update_service_account(&self, name: &str, opts: UpdateServiceAccountOpts) -> Result<OffsetDateTime> {
self.update_service_account_at(name, opts, OffsetDateTime::now_utc()).await
}
/// [`Self::update_service_account`] stamping the identity with
/// `updated_at` instead of the local clock; see [`Self::set_policy_at`]
/// (backlog#2291).
pub async fn update_service_account_at(
&self,
name: &str,
opts: UpdateServiceAccountOpts,
updated_at: OffsetDateTime,
) -> Result<OffsetDateTime> {
let _mutation_guard = self.cache.service_account_mutation_lock().lock().await;
let cache = self.cache.snapshot();
let Some(ui) = cache.users.get(name).cloned() else {
@@ -879,13 +908,7 @@ where
}
if let Some(status) = opts.status {
match status.as_str() {
val if val == AccountStatus::Enabled.as_ref() => cr.status = auth::ACCOUNT_ON.to_owned(),
val if val == AccountStatus::Disabled.as_ref() => cr.status = auth::ACCOUNT_OFF.to_owned(),
auth::ACCOUNT_ON => cr.status = auth::ACCOUNT_ON.to_owned(),
auth::ACCOUNT_OFF => cr.status = auth::ACCOUNT_OFF.to_owned(),
_ => cr.status = auth::ACCOUNT_OFF.to_owned(),
}
cr.status = account_status_flag(&status).to_owned();
}
let mut m: HashMap<String, Value> = if token_without_expiration {
@@ -937,8 +960,8 @@ where
cr.session_token = jwt_sign(&m, &cr.secret_key)?;
let u = UserIdentity::new(cr);
let updated_at = u.update_at.unwrap_or_else(OffsetDateTime::now_utc);
let mut u = UserIdentity::new(cr);
u.update_at = Some(updated_at);
self.api
.save_user_identity(&u.credentials.access_key, UserType::Svc, u.clone(), None)
.await?;
@@ -1170,6 +1193,20 @@ where
Ok((policies.into_iter().collect(), update_at))
}
pub async fn policy_db_set(&self, name: &str, user_type: UserType, is_group: bool, policy: &str) -> Result<OffsetDateTime> {
self.policy_db_set_at(name, user_type, is_group, policy, OffsetDateTime::now_utc())
.await
}
/// [`Self::policy_db_set`] stamping the mapping with `updated_at` instead
/// of the local clock; see [`Self::set_policy_at`] (backlog#2291).
pub async fn policy_db_set_at(
&self,
name: &str,
user_type: UserType,
is_group: bool,
policy: &str,
updated_at: OffsetDateTime,
) -> Result<OffsetDateTime> {
if name.is_empty() {
return Err(Error::InvalidArgument);
}
@@ -1189,10 +1226,11 @@ where
self.cache.delete_user_policy(name, OffsetDateTime::now_utc());
}
return Ok(OffsetDateTime::now_utc());
return Ok(updated_at);
}
let mp = MappedPolicy::new(policy);
let mut mp = MappedPolicy::new(policy);
mp.update_at = updated_at;
let cache = self.cache.snapshot();
let policy_docs_cache = Arc::clone(&cache.policy_docs);
@@ -1215,7 +1253,7 @@ where
self.cache.add_or_update_user_policy(name, &mp, OffsetDateTime::now_utc());
}
Ok(OffsetDateTime::now_utc())
Ok(updated_at)
}
pub async fn set_temp_user(&self, access_key: &str, cred: &Credentials, policy_name: Option<&str>) -> Result<OffsetDateTime> {
@@ -1412,6 +1450,17 @@ where
}
pub async fn add_user(&self, access_key: &str, args: &AddOrUpdateUserReq) -> Result<OffsetDateTime> {
self.add_user_at(access_key, args, OffsetDateTime::now_utc()).await
}
/// [`Self::add_user`] stamping the identity with `updated_at` instead of
/// the local clock; see [`Self::set_policy_at`] (backlog#2291).
pub async fn add_user_at(
&self,
access_key: &str,
args: &AddOrUpdateUserReq,
updated_at: OffsetDateTime,
) -> Result<OffsetDateTime> {
let cache = self.cache.snapshot();
let users = Arc::clone(&cache.users);
if let Some(x) = users.get(access_key) {
@@ -1429,12 +1478,13 @@ where
_ => auth::ACCOUNT_OFF,
}
};
let user_entry = UserIdentity::from(Credentials {
let mut user_entry = UserIdentity::from(Credentials {
access_key: access_key.to_string(),
secret_key: args.secret_key.to_string(),
status: status.to_owned(),
..Default::default()
});
user_entry.update_at = Some(updated_at);
self.api
.save_user_identity(access_key, UserType::Reg, user_entry.clone(), None)
@@ -1442,7 +1492,7 @@ where
self.update_user_with_claims(access_key, user_entry)?;
Ok(OffsetDateTime::now_utc())
Ok(updated_at)
}
pub async fn delete_user(&self, access_key: &str, utype: UserType) -> Result<()> {
@@ -1620,6 +1670,17 @@ where
}
pub async fn set_user_status(&self, access_key: &str, status: AccountStatus) -> Result<OffsetDateTime> {
self.set_user_status_at(access_key, status, OffsetDateTime::now_utc()).await
}
/// [`Self::set_user_status`] stamping the identity with `updated_at`
/// instead of the local clock; see [`Self::set_policy_at`] (backlog#2291).
pub async fn set_user_status_at(
&self,
access_key: &str,
status: AccountStatus,
updated_at: OffsetDateTime,
) -> Result<OffsetDateTime> {
if access_key.is_empty() {
return Err(Error::InvalidArgument);
}
@@ -1646,12 +1707,13 @@ where
}
};
let user_entry = UserIdentity::from(Credentials {
let mut user_entry = UserIdentity::from(Credentials {
access_key: access_key.to_string(),
secret_key: u.credentials.secret_key.clone(),
status: status.to_owned(),
..Default::default()
});
user_entry.update_at = Some(updated_at);
drop(cache);
drop(users);
@@ -1661,7 +1723,7 @@ where
self.update_user_with_claims(access_key, user_entry)?;
Ok(OffsetDateTime::now_utc())
Ok(updated_at)
}
fn update_user_with_claims(&self, k: &str, u: UserIdentity) -> Result<()> {
@@ -1697,6 +1759,17 @@ where
}
pub async fn add_users_to_group(&self, group: &str, members: Vec<String>) -> Result<OffsetDateTime> {
self.add_users_to_group_at(group, members, OffsetDateTime::now_utc()).await
}
/// [`Self::add_users_to_group`] stamping the group with `updated_at`
/// instead of the local clock; see [`Self::set_policy_at`] (backlog#2291).
pub async fn add_users_to_group_at(
&self,
group: &str,
members: Vec<String>,
updated_at: OffsetDateTime,
) -> Result<OffsetDateTime> {
if group.is_empty() {
return Err(Error::InvalidArgument);
}
@@ -1717,7 +1790,7 @@ where
// The group's own timestamp moves with every membership or status
// change: site replication judges an incoming group item against it
// (backlog#2291), so it must reflect the last change, not creation.
let now = OffsetDateTime::now_utc();
let now = updated_at;
let gi = match cache.groups.get(group) {
Some(res) => {
let mut gi = res.clone();
@@ -1729,7 +1802,11 @@ where
gi.update_at = Some(now);
gi
}
None => GroupInfo::new(members.clone()),
None => {
let mut gi = GroupInfo::new(members.clone());
gi.update_at = Some(now);
gi
}
};
drop(cache);
@@ -1750,6 +1827,12 @@ where
}
pub async fn set_group_status(&self, name: &str, enable: bool) -> Result<OffsetDateTime> {
self.set_group_status_at(name, enable, OffsetDateTime::now_utc()).await
}
/// [`Self::set_group_status`] stamping the group with `updated_at` instead
/// of the local clock; see [`Self::set_policy_at`] (backlog#2291).
pub async fn set_group_status_at(&self, name: &str, enable: bool, updated_at: OffsetDateTime) -> Result<OffsetDateTime> {
if name.is_empty() {
return Err(Error::InvalidArgument);
}
@@ -1767,7 +1850,7 @@ where
} else {
gi.status = STATUS_DISABLED.to_owned();
}
let now = OffsetDateTime::now_utc();
let now = updated_at;
gi.update_at = Some(now);
self.api.save_group_info(name, gi.clone()).await?;
@@ -1844,6 +1927,20 @@ where
name: &str,
members: Vec<String>,
update_cache_only: bool,
) -> Result<OffsetDateTime> {
self.remove_members_from_group_at(name, members, update_cache_only, OffsetDateTime::now_utc())
.await
}
/// [`Self::remove_members_from_group`] stamping the group with
/// `updated_at` instead of the local clock; see [`Self::set_policy_at`]
/// (backlog#2291).
pub async fn remove_members_from_group_at(
&self,
name: &str,
members: Vec<String>,
update_cache_only: bool,
updated_at: OffsetDateTime,
) -> Result<OffsetDateTime> {
let cache = self.cache.snapshot();
let mut gi = cache
@@ -1856,7 +1953,7 @@ where
let s: HashSet<&String> = HashSet::from_iter(gi.members.iter());
let d: HashSet<&String> = HashSet::from_iter(members.iter());
gi.members = s.difference(&d).map(|v| v.to_string()).collect::<Vec<String>>();
let now = OffsetDateTime::now_utc();
let now = updated_at;
gi.update_at = Some(now);
if !update_cache_only {
@@ -1880,6 +1977,19 @@ where
}
pub async fn remove_users_from_group(&self, group: &str, members: Vec<String>) -> Result<OffsetDateTime> {
self.remove_users_from_group_at(group, members, OffsetDateTime::now_utc())
.await
}
/// [`Self::remove_users_from_group`] stamping the group with `updated_at`
/// instead of the local clock; a group delete (no members) leaves no
/// record and returns the stamp unchanged (backlog#2291).
pub async fn remove_users_from_group_at(
&self,
group: &str,
members: Vec<String>,
updated_at: OffsetDateTime,
) -> Result<OffsetDateTime> {
if group.is_empty() {
return Err(Error::InvalidArgument);
}
@@ -1928,18 +2038,17 @@ where
return Err(err);
}
let now = self.cache.with_write_lock(|cache| {
self.cache.with_write_lock(|cache| {
let now = OffsetDateTime::now_utc();
self.remove_group_from_memberships_map_unlocked(cache, group, now);
cache.delete_group(group, now);
cache.delete_group_policy(group, now);
now
});
return Ok(now);
return Ok(updated_at);
}
self.remove_members_from_group(group, members, false).await
self.remove_members_from_group_at(group, members, false, updated_at).await
}
fn remove_group_from_memberships_map_unlocked(&self, cache: &mut LockedCache, group: &str, now: OffsetDateTime) {
@@ -2261,6 +2370,19 @@ where
}
}
/// The stored `status` flag for a service-account status given on the admin
/// or replication wire: the madmin `enabled` / `disabled` words and the stored
/// `on` / `off` flags are both accepted; anything else disables the account.
pub(crate) fn account_status_flag(status: &str) -> &'static str {
match status {
val if val == AccountStatus::Enabled.as_ref() => auth::ACCOUNT_ON,
val if val == AccountStatus::Disabled.as_ref() => auth::ACCOUNT_OFF,
auth::ACCOUNT_ON => auth::ACCOUNT_ON,
auth::ACCOUNT_OFF => auth::ACCOUNT_OFF,
_ => auth::ACCOUNT_OFF,
}
}
pub fn get_default_policies() -> HashMap<String, PolicyDoc> {
let default_policies = &DEFAULT_POLICIES;
default_policies
+123 -10
View File
@@ -385,7 +385,14 @@ impl<T: Store> IamSys<T> {
}
pub async fn set_policy(&self, name: &str, policy: Policy) -> Result<OffsetDateTime> {
let updated_at = self.store.set_policy(name, policy).await?;
self.set_policy_at(name, policy, OffsetDateTime::now_utc()).await
}
/// [`Self::set_policy`] stamping the document with `updated_at` (a
/// replicated edit's source time) instead of the local clock; see
/// `IamCache::set_policy_at` (backlog#2291).
pub async fn set_policy_at(&self, name: &str, policy: Policy, updated_at: OffsetDateTime) -> Result<OffsetDateTime> {
let updated_at = self.store.set_policy_at(name, policy, updated_at).await?;
if !self.has_watcher() {
for r in notify_iam_load_policy(name).await {
@@ -643,7 +650,18 @@ impl<T: Store> IamSys<T> {
}
pub async fn set_user_status(&self, name: &str, status: rustfs_madmin::AccountStatus) -> Result<OffsetDateTime> {
let updated_at = self.store.set_user_status(name, status).await?;
self.set_user_status_at(name, status, OffsetDateTime::now_utc()).await
}
/// [`Self::set_user_status`] stamping the identity with `updated_at` (a
/// replicated edit's source time) instead of the local clock (backlog#2291).
pub async fn set_user_status_at(
&self,
name: &str,
status: rustfs_madmin::AccountStatus,
updated_at: OffsetDateTime,
) -> Result<OffsetDateTime> {
let updated_at = self.store.set_user_status_at(name, status, updated_at).await?;
self.notify_for_user(name, false).await;
@@ -655,6 +673,20 @@ impl<T: Store> IamSys<T> {
parent_user: &str,
groups: Option<Vec<String>>,
opts: NewServiceAccountOpts,
) -> Result<(Credentials, OffsetDateTime)> {
self.new_service_account_at(parent_user, groups, opts, OffsetDateTime::now_utc())
.await
}
/// [`Self::new_service_account`] stamping the identity with `updated_at`
/// (a replicated edit's source time) instead of the local clock
/// (backlog#2291).
pub async fn new_service_account_at(
&self,
parent_user: &str,
groups: Option<Vec<String>>,
opts: NewServiceAccountOpts,
updated_at: OffsetDateTime,
) -> Result<(Credentials, OffsetDateTime)> {
if parent_user.is_empty() {
return Err(IamError::InvalidArgument);
@@ -724,11 +756,18 @@ impl<T: Store> IamSys<T> {
let mut cred = create_new_credentials_with_metadata(&access_key, &secret_key, &m, &secret_key)?;
cred.parent_user = parent_user.to_owned();
cred.groups = groups;
cred.status = ACCOUNT_ON.to_owned();
// The status is part of the created identity: a replicated disabled
// account must never exist enabled, not even between a create and a
// follow-up status write (backlog#2289).
cred.status = opts
.status
.as_deref()
.map_or(ACCOUNT_ON, crate::manager::account_status_flag)
.to_owned();
cred.name = opts.name;
cred.description = opts.description;
let create_at = self.store.add_service_account(cred.clone()).await?;
let create_at = self.store.add_service_account_at(cred.clone(), updated_at).await?;
self.notify_for_service_account(&cred.access_key).await;
@@ -736,11 +775,23 @@ impl<T: Store> IamSys<T> {
}
pub async fn update_service_account(&self, name: &str, opts: UpdateServiceAccountOpts) -> Result<OffsetDateTime> {
self.update_service_account_at(name, opts, OffsetDateTime::now_utc()).await
}
/// [`Self::update_service_account`] stamping the identity with
/// `updated_at` (a replicated edit's source time) instead of the local
/// clock (backlog#2291).
pub async fn update_service_account_at(
&self,
name: &str,
opts: UpdateServiceAccountOpts,
updated_at: OffsetDateTime,
) -> Result<OffsetDateTime> {
if name == SITE_REPLICATOR_SERVICE_ACCOUNT && !opts.allow_site_replicator_account {
return Err(IamError::IAMActionNotAllowed);
}
let updated_at = self.store.update_service_account(name, opts).await?;
let updated_at = self.store.update_service_account_at(name, opts, updated_at).await?;
self.notify_for_service_account(name).await;
@@ -940,6 +991,17 @@ impl<T: Store> IamSys<T> {
}
pub async fn create_user(&self, access_key: &str, args: &AddOrUpdateUserReq) -> Result<OffsetDateTime> {
self.create_user_at(access_key, args, OffsetDateTime::now_utc()).await
}
/// [`Self::create_user`] stamping the identity with `updated_at` (a
/// replicated edit's source time) instead of the local clock (backlog#2291).
pub async fn create_user_at(
&self,
access_key: &str,
args: &AddOrUpdateUserReq,
updated_at: OffsetDateTime,
) -> Result<OffsetDateTime> {
if !is_access_key_valid(access_key) {
return Err(IamError::InvalidAccessKeyLength);
}
@@ -952,7 +1014,7 @@ impl<T: Store> IamSys<T> {
return Err(IamError::InvalidSecretKeyLength);
}
let updated_at = self.store.add_user(access_key, args).await?;
let updated_at = self.store.add_user_at(access_key, args, updated_at).await?;
self.load_user(access_key, UserType::Reg).await?;
self.notify_for_user(access_key, false).await;
@@ -1026,10 +1088,21 @@ impl<T: Store> IamSys<T> {
}
pub async fn add_users_to_group(&self, group: &str, users: Vec<String>) -> Result<OffsetDateTime> {
self.add_users_to_group_at(group, users, OffsetDateTime::now_utc()).await
}
/// [`Self::add_users_to_group`] stamping the group with `updated_at` (a
/// replicated edit's source time) instead of the local clock (backlog#2291).
pub async fn add_users_to_group_at(
&self,
group: &str,
users: Vec<String>,
updated_at: OffsetDateTime,
) -> Result<OffsetDateTime> {
if contains_reserved_chars(group) {
return Err(IamError::GroupNameContainsReservedChars);
}
let updated_at = self.store.add_users_to_group(group, users).await?;
let updated_at = self.store.add_users_to_group_at(group, users, updated_at).await?;
self.notify_for_group(group).await;
@@ -1037,7 +1110,19 @@ impl<T: Store> IamSys<T> {
}
pub async fn remove_users_from_group(&self, group: &str, users: Vec<String>) -> Result<OffsetDateTime> {
let updated_at = self.store.remove_users_from_group(group, users).await?;
self.remove_users_from_group_at(group, users, OffsetDateTime::now_utc()).await
}
/// [`Self::remove_users_from_group`] stamping the group with `updated_at`
/// (a replicated edit's source time) instead of the local clock
/// (backlog#2291).
pub async fn remove_users_from_group_at(
&self,
group: &str,
users: Vec<String>,
updated_at: OffsetDateTime,
) -> Result<OffsetDateTime> {
let updated_at = self.store.remove_users_from_group_at(group, users, updated_at).await?;
self.notify_for_group(group).await;
@@ -1045,7 +1130,13 @@ impl<T: Store> IamSys<T> {
}
pub async fn set_group_status(&self, group: &str, enable: bool) -> Result<OffsetDateTime> {
let updated_at = self.store.set_group_status(group, enable).await?;
self.set_group_status_at(group, enable, OffsetDateTime::now_utc()).await
}
/// [`Self::set_group_status`] stamping the group with `updated_at` (a
/// replicated edit's source time) instead of the local clock (backlog#2291).
pub async fn set_group_status_at(&self, group: &str, enable: bool, updated_at: OffsetDateTime) -> Result<OffsetDateTime> {
let updated_at = self.store.set_group_status_at(group, enable, updated_at).await?;
self.notify_for_group(group).await;
@@ -1080,7 +1171,24 @@ impl<T: Store> IamSys<T> {
}
pub async fn policy_db_set(&self, name: &str, user_type: UserType, is_group: bool, policy: &str) -> Result<OffsetDateTime> {
let updated_at = self.store.policy_db_set(name, user_type, is_group, policy).await?;
self.policy_db_set_at(name, user_type, is_group, policy, OffsetDateTime::now_utc())
.await
}
/// [`Self::policy_db_set`] stamping the mapping with `updated_at` (a
/// replicated edit's source time) instead of the local clock (backlog#2291).
pub async fn policy_db_set_at(
&self,
name: &str,
user_type: UserType,
is_group: bool,
policy: &str,
updated_at: OffsetDateTime,
) -> Result<OffsetDateTime> {
let updated_at = self
.store
.policy_db_set_at(name, user_type, is_group, policy, updated_at)
.await?;
if !self.has_watcher() {
for r in notify_iam_load_policy_mapping(name, user_type.to_u64(), is_group).await {
@@ -1862,6 +1970,11 @@ pub struct NewServiceAccountOpts {
pub expiration: Option<OffsetDateTime>,
pub allow_site_replicator_account: bool,
pub claims: Option<HashMap<String, Value>>,
/// Status the account is created with (`enabled` / `disabled` or the
/// stored `on` / `off` flags); `None` creates it enabled. Site
/// replication passes the source account's status so a disabled account
/// is never enabled on the peer, not even transiently (backlog#2289).
pub status: Option<String>,
}
pub struct UpdateServiceAccountOpts {
+18 -3
View File
@@ -45,18 +45,33 @@ pub struct PolicyDoc {
impl PolicyDoc {
pub fn new(policy: Policy) -> Self {
Self::new_at(policy, OffsetDateTime::now_utc())
}
/// [`Self::new`] with an explicit `UpdateDate` (and `CreateDate`).
///
/// A replicated document keeps the edit's source time: the receiver
/// judges the next incoming revision against the stored stamp, so a
/// local stamp would reject a newer source edit that was merely
/// delivered later.
pub fn new_at(policy: Policy, at: OffsetDateTime) -> Self {
Self {
version: 1,
policy,
create_date: Some(OffsetDateTime::now_utc()),
update_date: Some(OffsetDateTime::now_utc()),
create_date: Some(at),
update_date: Some(at),
}
}
pub fn update(&mut self, policy: Policy) {
self.update_at(policy, OffsetDateTime::now_utc());
}
/// [`Self::update`] with an explicit `UpdateDate`; see [`Self::new_at`].
pub fn update_at(&mut self, policy: Policy, at: OffsetDateTime) {
self.version += 1;
self.policy = policy;
self.update_date = Some(OffsetDateTime::now_utc());
self.update_date = Some(at);
if self.create_date.is_none() {
self.create_date = self.update_date;