mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-24 13:16:28 +00:00
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:
+119
-1
@@ -139,6 +139,59 @@ impl UserType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode a [`UserType`] as the site-replication wire value for
|
||||
/// `SRPolicyMapping.userType` / `SRCredInfo.iamUserType`.
|
||||
///
|
||||
/// The wire uses MinIO's `IAMUserType` table (cmd/iam.go):
|
||||
///
|
||||
/// | wire | MinIO meaning |
|
||||
/// |------|---------------|
|
||||
/// | -1 | unknown |
|
||||
/// | 0 | regUser |
|
||||
/// | 1 | stsUser |
|
||||
/// | 2 | svcUser |
|
||||
///
|
||||
/// This is deliberately distinct from the internal encoding
|
||||
/// [`UserType::to_u64`]/[`UserType::from_u64`] (None=0, Svc=1, Sts=2, Reg=3),
|
||||
/// which is used by intra-cluster node RPC and must never change (a rolling
|
||||
/// restart mixes old and new nodes on that RPC). Do not "unify" the two
|
||||
/// tables: internal values on the SR wire mislabel users on MinIO peers.
|
||||
///
|
||||
/// Group mappings always encode as 0: MinIO routes group mappings by the
|
||||
/// `isGroup` flag (userType is effectively ignored), and pre-fix RustFS peers
|
||||
/// sent 0 for groups, so 0 is the one value every peer generation accepts.
|
||||
pub fn sr_wire_user_type(user_type: UserType, is_group: bool) -> i64 {
|
||||
if is_group {
|
||||
return 0;
|
||||
}
|
||||
match user_type {
|
||||
UserType::Reg | UserType::None => 0,
|
||||
UserType::Sts => 1,
|
||||
UserType::Svc => 2,
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode a site-replication wire `userType` value (see [`sr_wire_user_type`]
|
||||
/// for the table) into a [`UserType`].
|
||||
///
|
||||
/// - `-1` (MinIO unknown, sent for group mappings) maps to [`UserType::None`];
|
||||
/// `policy_db_set` routes group items by `is_group`, and for non-group items
|
||||
/// `None` shares the users prefix with `Reg`.
|
||||
/// - `3` is a permanent alias for [`UserType::Reg`]: pre-fix RustFS peers sent
|
||||
/// the internal encoding (`Reg.to_u64() == 3`) on the wire. Keep it forever
|
||||
/// for mixed-version site replication; do not remove.
|
||||
/// - Anything else is unknown and rejected (`None`), so callers fail closed.
|
||||
pub fn user_type_from_sr_wire(v: i64) -> Option<UserType> {
|
||||
match v {
|
||||
-1 => Some(UserType::None),
|
||||
0 => Some(UserType::Reg),
|
||||
1 => Some(UserType::Sts),
|
||||
2 => Some(UserType::Svc),
|
||||
3 => Some(UserType::Reg),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct MappedPolicy {
|
||||
pub version: i64,
|
||||
@@ -214,7 +267,72 @@ impl GroupInfo {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{GroupInfo, MappedPolicy};
|
||||
use super::{GroupInfo, MappedPolicy, UserType, sr_wire_user_type, user_type_from_sr_wire};
|
||||
|
||||
/// Site-replication inbound decode of `SRPolicyMapping.userType` must
|
||||
/// follow MinIO IAMUserType wire semantics (cmd/iam.go): stsUser = 1.
|
||||
/// The internal `UserType::from_u64` table maps 1 to Svc — reusing it at
|
||||
/// the SR boundary lands federated STS mappings under the wrong prefix
|
||||
/// and silently drops their effect.
|
||||
#[test]
|
||||
fn sr_inbound_decodes_minio_sts_wire_value_as_sts() {
|
||||
assert_eq!(user_type_from_sr_wire(1), Some(UserType::Sts));
|
||||
}
|
||||
|
||||
/// Wire-constant contract: literal MinIO IAMUserType values (cmd/iam.go).
|
||||
/// WARNING: these literals are the cross-vendor wire format. Never "tidy"
|
||||
/// them to match `UserType::to_u64`/`from_u64` — that internal table
|
||||
/// (None=0, Svc=1, Sts=2, Reg=3) belongs to intra-cluster node RPC only.
|
||||
#[test]
|
||||
fn sr_wire_decode_matches_minio_iam_user_type_table() {
|
||||
assert_eq!(user_type_from_sr_wire(-1), Some(UserType::None)); // MinIO unknown (group mappings)
|
||||
assert_eq!(user_type_from_sr_wire(0), Some(UserType::Reg)); // MinIO regUser
|
||||
assert_eq!(user_type_from_sr_wire(1), Some(UserType::Sts)); // MinIO stsUser
|
||||
assert_eq!(user_type_from_sr_wire(2), Some(UserType::Svc)); // MinIO svcUser
|
||||
// Permanent alias: pre-fix RustFS peers sent internal Reg=3 on the wire.
|
||||
assert_eq!(user_type_from_sr_wire(3), Some(UserType::Reg));
|
||||
// Unknown values fail closed.
|
||||
assert_eq!(user_type_from_sr_wire(4), None);
|
||||
assert_eq!(user_type_from_sr_wire(-2), None);
|
||||
}
|
||||
|
||||
/// Wire-constant contract for the outbound direction.
|
||||
#[test]
|
||||
fn sr_wire_encode_matches_minio_iam_user_type_table() {
|
||||
assert_eq!(sr_wire_user_type(UserType::Reg, false), 0); // MinIO regUser
|
||||
assert_eq!(sr_wire_user_type(UserType::Sts, false), 1); // MinIO stsUser
|
||||
assert_eq!(sr_wire_user_type(UserType::Svc, false), 2); // MinIO svcUser
|
||||
assert_eq!(sr_wire_user_type(UserType::None, false), 0);
|
||||
// Group mappings always go out as 0 — the value both MinIO (routes by
|
||||
// isGroup) and pre-fix RustFS peers accept.
|
||||
for ut in [UserType::Reg, UserType::Sts, UserType::Svc, UserType::None] {
|
||||
assert_eq!(sr_wire_user_type(ut, true), 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mixed-version matrix: every value a peer generation can emit decodes to
|
||||
/// a `UserType` the receiver stores correctly.
|
||||
#[test]
|
||||
fn sr_wire_round_trip_covers_old_rustfs_and_minio_peers() {
|
||||
// Old RustFS outbound: user mappings as internal Reg=3, groups as 0.
|
||||
assert_eq!(user_type_from_sr_wire(3), Some(UserType::Reg));
|
||||
assert_eq!(user_type_from_sr_wire(0), Some(UserType::Reg));
|
||||
// New RustFS outbound decodes on its own kind (self round-trip).
|
||||
for (ut, is_group) in [
|
||||
(UserType::Reg, false),
|
||||
(UserType::Sts, false),
|
||||
(UserType::Svc, false),
|
||||
(UserType::None, true),
|
||||
] {
|
||||
assert!(user_type_from_sr_wire(sr_wire_user_type(ut, is_group)).is_some());
|
||||
}
|
||||
// Internal RPC encoding is untouched (rolling-restart contract).
|
||||
assert_eq!(UserType::None.to_u64(), 0);
|
||||
assert_eq!(UserType::Svc.to_u64(), 1);
|
||||
assert_eq!(UserType::Sts.to_u64(), 2);
|
||||
assert_eq!(UserType::Reg.to_u64(), 3);
|
||||
assert_eq!(UserType::from_u64(1), Some(UserType::Svc));
|
||||
}
|
||||
|
||||
/// uses RFC3339 for updatedAt. MappedPolicy must serialize as RFC3339.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user