fix(site-replication): translate policy mapping userType at MinIO wire boundary (#5751)

* test(site-replication): pin MinIO IAMUserType wire semantics for policy mappings

Red tests for P0-4: MinIO peers send SRPolicyMapping.UserType using the
madmin IAMUserType table (unknown=-1, regUser=0, stsUser=1, svcUser=2),
while RustFS deserializes the field as u64 and decodes it with the
internal RPC table (None=0, Svc=1, Sts=2, Reg=3).

- userType -1 (MinIO group mappings) fails to deserialize, rejecting the
  whole IAM item: group mappings never sync from MinIO.
- stsUser=1 decodes as Svc, landing federated STS mappings under the
  wrong prefix and silently dropping their effect.

* fix(site-replication): translate policy mapping userType at MinIO wire boundary

SRPolicyMapping.userType travels on the wire using MinIO's IAMUserType
table (unknown=-1, regUser=0, stsUser=1, svcUser=2), but RustFS stored
the field as u64 and reused the internal RPC encoding
UserType::to_u64/from_u64 (None=0, Svc=1, Sts=2, Reg=3) at the site
replication boundary. Consequences: MinIO group mappings (userType -1)
failed to deserialize and the whole IAM item was rejected, and MinIO STS
mappings (1) were stored as service-account mappings, silently dropping
federated users' policies.

- Widen SRPolicyMapping.user_type and SRCredInfo.iam_user_type to i64 so
  MinIO's -1 deserializes.
- Add sr_wire_user_type / user_type_from_sr_wire in rustfs-iam as the
  dedicated SR wire codec: MinIO table on both directions, groups always
  encoded as 0, and wire value 3 kept forever as an alias for Reg so
  mappings from pre-fix RustFS peers still decode; unknown values fail
  closed.
- Route the SR inbound (apply_iam_item) and outbound
  (mapped_policy_to_sr_mapping, policy-mapping change hooks) paths
  through the codec.

The internal UserType::to_u64/from_u64 encoding is untouched: it is the
intra-cluster node RPC contract and changing it would break rolling
restarts. Outbound compatibility with old RustFS peers is preserved
because UserType::None and Reg share the users prefix in
get_mapped_policy_path, so wire 0 lands in the same location Reg=3 did.
This commit is contained in:
唐小鸭
2026-08-06 22:00:28 +08:00
committed by GitHub
parent 4855095446
commit 6303aa9a42
4 changed files with 164 additions and 10 deletions
+2 -2
View File
@@ -571,7 +571,7 @@ impl Operation for SetPolicyForUserOrGroup {
r#type: "policy-mapping".to_string(),
policy_mapping: Some(SRPolicyMapping {
user_or_group: query.user_or_group.clone(),
user_type: rustfs_iam::store::UserType::Reg.to_u64(),
user_type: rustfs_iam::store::sr_wire_user_type(rustfs_iam::store::UserType::Reg, query.is_group),
is_group: query.is_group,
policy: query.policy_name.clone(),
updated_at: Some(updated_at),
@@ -1060,7 +1060,7 @@ pub(crate) async fn handle_builtin_policy_association(
r#type: "policy-mapping".to_string(),
policy_mapping: Some(SRPolicyMapping {
user_or_group: target_name.clone(),
user_type: rustfs_iam::store::UserType::Reg.to_u64(),
user_type: rustfs_iam::store::sr_wire_user_type(rustfs_iam::store::UserType::Reg, is_group),
is_group,
policy: updated_policies.join(","),
updated_at: Some(updated_at),
@@ -63,7 +63,7 @@ use rustfs_config::{
};
use rustfs_iam::error::is_err_no_such_service_account;
use rustfs_iam::federation::OIDC_VIRTUAL_PARENT_CLAIM;
use rustfs_iam::store::{MappedPolicy, UserType};
use rustfs_iam::store::{MappedPolicy, UserType, sr_wire_user_type, user_type_from_sr_wire};
use rustfs_iam::sys::{
NewServiceAccountOpts, SITE_REPLICATOR_SERVICE_ACCOUNT, UpdateServiceAccountOpts, get_claims_from_token_with_secret,
};
@@ -4303,7 +4303,7 @@ fn local_idp_settings() -> IDPSettings {
fn mapped_policy_to_sr_mapping(name: String, is_group: bool, user_type: UserType, mapping: MappedPolicy) -> SRPolicyMapping {
SRPolicyMapping {
user_or_group: name,
user_type: user_type.to_u64(),
user_type: sr_wire_user_type(user_type, is_group),
is_group,
policy: mapping.policies,
updated_at: Some(mapping.update_at),
@@ -7824,7 +7824,8 @@ async fn apply_iam_item(item: SRIAMItem) -> S3Result<()> {
let Some(mapping) = item.policy_mapping else {
return Err(s3_error!(InvalidRequest, "policyMapping is required"));
};
let user_type = UserType::from_u64(mapping.user_type).ok_or_else(|| s3_error!(InvalidRequest, "invalid userType"))?;
let user_type =
user_type_from_sr_wire(mapping.user_type).ok_or_else(|| s3_error!(InvalidRequest, "invalid userType"))?;
iam_sys
.policy_db_set(&mapping.user_or_group, user_type, mapping.is_group, &mapping.policy)
.await
@@ -11910,7 +11911,7 @@ mod tests {
"alice".to_string(),
SRPolicyMapping {
user_or_group: "alice".to_string(),
user_type: UserType::Reg.to_u64(),
user_type: sr_wire_user_type(UserType::Reg, false),
policy: "readwrite".to_string(),
updated_at: Some(OffsetDateTime::UNIX_EPOCH),
..Default::default()