diff --git a/crates/ecstore/src/bucket/target/arn.rs b/crates/ecstore/src/bucket/target/arn.rs index 0bf0f175a..e680daca9 100644 --- a/crates/ecstore/src/bucket/target/arn.rs +++ b/crates/ecstore/src/bucket/target/arn.rs @@ -40,7 +40,14 @@ impl ARN { impl Display for ARN { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "arn:rustfs:{}:{}:{}:{}", self.arn_type, self.region, self.id, self.bucket) + // The `minio` partition is deliberate: madmin-go's ParseARN + // hard-rejects any other partition, so native mc/madmin tooling can + // only decode remote-target ARNs minted in this form (backlog#1675 + // P1-7). Legacy `arn:rustfs:` ARNs persisted by older releases stay + // readable via the FromStr whitelist below; runtime matching between + // targets and replication rules is by full-string equality, so mixed + // partitions coexist safely. + write!(f, "arn:minio:{}:{}:{}:{}", self.arn_type, self.region, self.id, self.bucket) } } @@ -48,7 +55,12 @@ impl FromStr for ARN { type Err = std::io::Error; fn from_str(s: &str) -> Result { - if !s.starts_with("arn:rustfs:") { + // Partition whitelist, not just an `arn:` check: `BucketTargetType:: + // from_str(...).unwrap_or_default()` below never fails, so this is + // the only structural gate rejecting foreign ARNs. `arn:rustfs:` is + // the legacy partition and must stay accepted forever (persisted + // bucket-targets.json / replication configs from older releases). + if !s.starts_with("arn:minio:") && !s.starts_with("arn:rustfs:") { return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid ARN format")); } @@ -101,14 +113,50 @@ mod tests { } /// RustFS commonly generates ARNs with an empty region: - /// `arn:rustfs:replication:::`. + /// `arn:minio:replication:::`. #[test] fn from_str_handles_empty_region_segment() { - let parsed = ARN::from_str("arn:rustfs:replication::depl-123:bucket-a").expect("valid ARN must parse"); + let parsed = ARN::from_str("arn:minio:replication::depl-123:bucket-a").expect("valid ARN must parse"); assert_eq!(parsed.arn_type, BucketTargetType::ReplicationService); assert_eq!(parsed.region, "", "region segment is empty in this form"); assert_eq!(parsed.id, "depl-123"); assert_eq!(parsed.bucket, "bucket-a"); } + + /// madmin-go's `ParseARN` hard-rejects anything that does not start with + /// `arn:minio:`, so generated ARNs must use the `minio` partition or the + /// native mc/madmin tooling cannot decode remote-target listings. + #[test] + fn display_emits_minio_partition() { + let arn = ARN::new( + BucketTargetType::ReplicationService, + "depl-123".to_string(), + String::new(), + "bucket-a".to_string(), + ); + + assert_eq!(arn.to_string(), "arn:minio:replication::depl-123:bucket-a"); + } + + /// Persisted bucket-targets.json files from older RustFS releases carry + /// `arn:rustfs:` ARNs; the legacy partition must stay parseable forever. + #[test] + fn from_str_accepts_legacy_rustfs_partition() { + let parsed = ARN::from_str("arn:rustfs:replication:us-east-1:depl-123:bucket-a").expect("legacy ARN must parse"); + + assert_eq!(parsed.arn_type, BucketTargetType::ReplicationService); + assert_eq!(parsed.region, "us-east-1"); + assert_eq!(parsed.id, "depl-123"); + assert_eq!(parsed.bucket, "bucket-a"); + } + + /// The partition whitelist is the only structural gate: `BucketTargetType:: + /// from_str(...).unwrap_or_default()` never fails, so any 6-segment string + /// would otherwise parse as `type=None`. + #[test] + fn from_str_rejects_unknown_partition() { + assert!(ARN::from_str("arn:aws:replication::depl-123:bucket-a").is_err()); + assert!(ARN::from_str("not-an-arn").is_err()); + } } diff --git a/rustfs/src/admin/handlers/site_replication.rs b/rustfs/src/admin/handlers/site_replication.rs index 495db4284..d9604fe6f 100644 --- a/rustfs/src/admin/handlers/site_replication.rs +++ b/rustfs/src/admin/handlers/site_replication.rs @@ -15305,7 +15305,10 @@ mod tests { assert!(!target.secure); assert_eq!(target.target_bucket, "photos"); assert_eq!(target.deployment_id, "remote"); - assert_eq!(target.arn, "arn:rustfs:replication::remote:photos"); + // Freshly minted ARNs use the `minio` partition so madmin-go tooling + // can parse them; legacy `arn:rustfs:` targets are preserved as-is + // (see the MinIO-era preservation test below). + assert_eq!(target.arn, "arn:minio:replication::remote:photos"); assert_eq!(target.region, "us-east-1"); let credentials = target .credentials