feat: improve legacy metadata and admin compatibility (#2202)

This commit is contained in:
weisd
2026-03-18 21:05:09 +08:00
committed by GitHub
parent 84077adf17
commit b9b7d86ae4
133 changed files with 11707 additions and 1945 deletions
+18
View File
@@ -26,6 +26,8 @@ use time::OffsetDateTime;
pub struct UserIdentity {
pub version: i64,
pub credentials: Credentials,
/// updatedAt (RFC3339), legacy RustFS: update_at. Serialize as updatedAt
#[serde(rename = "updatedAt", alias = "update_at", default, with = "crate::serde_datetime::option")]
pub update_at: Option<OffsetDateTime>,
}
@@ -74,3 +76,19 @@ impl From<Credentials> for UserIdentity {
}
}
}
#[cfg(test)]
mod tests {
use super::UserIdentity;
/// Deserialize UserIdentity from MinIO-style JSON (RFC3339 updatedAt).
#[test]
fn test_user_identity_deserialize_minio_style_rfc3339() {
let minio_style =
r#"{"version":1,"credentials":{"accessKey":"ak","secretKey":"sk12345678"},"updatedAt":"2025-03-07T12:00:00Z"}"#;
let u: UserIdentity = serde_json::from_str(minio_style).expect("deserialize MinIO-style identity");
assert_eq!(u.version, 1);
assert_eq!(u.credentials.access_key, "ak");
assert!(u.update_at.is_some());
}
}
+1
View File
@@ -17,5 +17,6 @@ pub mod auth;
pub mod error;
pub mod format;
pub mod policy;
pub mod serde_datetime;
pub mod service_type;
pub mod utils;
+58
View File
@@ -19,9 +19,27 @@ use super::Policy;
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct PolicyDoc {
/// Version (omitempty), legacy: version.
#[serde(rename = "Version", alias = "version", default)]
pub version: i64,
/// Policy, legacy: policy. Serialize as Policy.
#[serde(rename = "Policy", alias = "policy")]
pub policy: Policy,
/// CreateDate (RFC3339), legacy: create_date. Serialize as CreateDate.
#[serde(
rename = "CreateDate",
alias = "create_date",
default,
with = "crate::serde_datetime::option"
)]
pub create_date: Option<OffsetDateTime>,
/// UpdateDate (RFC3339), legacy: update_date. Serialize as UpdateDate.
#[serde(
rename = "UpdateDate",
alias = "update_date",
default,
with = "crate::serde_datetime::option"
)]
pub update_date: Option<OffsetDateTime>,
}
@@ -73,3 +91,43 @@ impl TryFrom<Vec<u8>> for PolicyDoc {
.map_err(|_| serde_json::Error::custom("Failed to parse as PolicyDoc or Policy".to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::policy::Policy;
#[test]
fn test_policy_doc_timestamps_serialize_as_rfc3339() {
let policy = Policy::default();
let doc = PolicyDoc::new(policy);
let json = serde_json::to_string(&doc).expect("serialize");
// RFC3339 uses 'T' between date and time and 'Z' or offset for UTC
assert!(json.contains('T'), "PolicyDoc timestamps should be RFC3339 (contain 'T'); got: {}", json);
assert!(
json.contains('Z') || json.contains("+00:00"),
"PolicyDoc timestamps should be RFC3339 (contain 'Z' or +00:00); got: {}",
json
);
}
#[test]
fn test_policy_doc_deserialize_minio_style_rfc3339_timestamps() {
let minio_style = r#"{"Version":1,"Policy":{"Version":"2012-10-17","Statement":[]},"CreateDate":"2025-03-07T12:00:00Z","UpdateDate":"2025-03-07T12:00:00Z"}"#;
let doc: PolicyDoc = serde_json::from_str(minio_style).expect("deserialize MinIO-style JSON");
assert_eq!(doc.version, 1);
assert!(doc.create_date.is_some());
assert!(doc.update_date.is_some());
}
/// Round-trip: serialize then deserialize PolicyDoc; timestamps must match.
#[test]
fn test_policy_doc_timestamp_roundtrip() {
let policy = Policy::default();
let doc = PolicyDoc::new(policy);
let json = serde_json::to_string(&doc).expect("serialize");
let restored: PolicyDoc = serde_json::from_str(&json).expect("deserialize");
assert_eq!(doc.create_date, restored.create_date);
assert_eq!(doc.update_date, restored.update_date);
}
}
+10 -2
View File
@@ -55,9 +55,9 @@ impl Args<'_> {
pub struct Policy {
#[serde(default, rename = "ID")]
pub id: ID,
#[serde(rename = "Version")]
#[serde(default, rename = "Version")]
pub version: String,
#[serde(rename = "Statement")]
#[serde(default, rename = "Statement")]
pub statements: Vec<Statement>,
}
@@ -1084,6 +1084,14 @@ mod test {
);
}
#[test]
fn test_parse_empty_policy_object_as_implied_policy() {
let policy = Policy::parse_config(b"{}").expect("empty JSON object should parse");
assert!(policy.version.is_empty());
assert!(policy.statements.is_empty());
}
#[test]
fn test_statement_with_both_action_and_notaction_is_invalid() {
// Test: A statement with both Action and NotAction returns BothActionAndNotAction error
+121
View File
@@ -0,0 +1,121 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Serde helpers for IAM timestamps: serialize as RFC3339 (MinIO-compatible),
//! deserialize from RFC3339 or legacy RustFS human-readable format.
use serde::{Deserialize, Deserializer, Serializer};
use time::OffsetDateTime;
use time::format_description;
use time::format_description::well_known::Rfc3339;
/// Legacy RustFS format: `YYYY-MM-DD HH:MM:SS.ffffff +00:00:00` (time crate serde-human-readable style).
static LEGACY_FORMAT: std::sync::OnceLock<time::format_description::OwnedFormatItem> = std::sync::OnceLock::new();
fn legacy_format() -> &'static time::format_description::OwnedFormatItem {
LEGACY_FORMAT.get_or_init(|| {
format_description::parse_owned::<2>(
"[year]-[month]-[day] [hour]:[minute]:[second].[subsecond] [offset_hour sign:mandatory]:[offset_minute]:[offset_second]",
)
.expect("legacy format description is valid")
});
LEGACY_FORMAT.get().expect("initialized above")
}
fn parse_rfc3339_or_legacy(s: &str) -> Result<OffsetDateTime, time::Error> {
OffsetDateTime::parse(s, &Rfc3339).or_else(|_| OffsetDateTime::parse(s, legacy_format()).map_err(Into::into))
}
/// Serialize as RFC3339; deserialize from RFC3339 or legacy RustFS format.
pub fn serialize<S>(dt: &OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
time::serde::rfc3339::serialize(dt, serializer)
}
/// Deserialize from RFC3339 or legacy RustFS human-readable format.
pub fn deserialize<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error>
where
D: Deserializer<'de>,
{
let s = <&str>::deserialize(deserializer)?;
parse_rfc3339_or_legacy(s).map_err(serde::de::Error::custom)
}
/// Option version: serialize as RFC3339; deserialize from RFC3339 or legacy.
pub mod option {
use serde::{Deserialize, Deserializer, Serializer};
use time::OffsetDateTime;
use super::{Rfc3339, parse_rfc3339_or_legacy};
pub fn serialize<S>(opt: &Option<OffsetDateTime>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match opt {
Some(dt) => {
let s = dt.format(&Rfc3339).map_err(serde::ser::Error::custom)?;
serializer.serialize_some(&s)
}
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<OffsetDateTime>, D::Error>
where
D: Deserializer<'de>,
{
let opt: Option<&str> = Option::deserialize(deserializer)?;
match opt {
None => Ok(None),
Some(s) => parse_rfc3339_or_legacy(s).map(Some).map_err(serde::de::Error::custom),
}
}
}
#[cfg(test)]
mod tests {
use serde::{Deserialize, Serialize};
use super::*;
#[derive(Serialize, Deserialize)]
#[serde(transparent)]
struct Dt(#[serde(with = "crate::serde_datetime")] OffsetDateTime);
#[test]
fn test_deserialize_legacy_rustfs_timestamp() {
// Legacy RustFS human-readable format (time serde-human-readable).
let json = r#""2026-03-09 02:22:44.998954 +00:00:00""#;
let Dt(dt) = serde_json::from_str(json).expect("deserialize legacy timestamp");
assert_eq!(dt.year(), 2026);
assert_eq!(dt.month(), time::Month::March);
assert_eq!(dt.day(), 9);
assert_eq!(dt.hour(), 2);
assert_eq!(dt.minute(), 22);
assert_eq!(dt.second(), 44);
}
#[test]
fn test_deserialize_rfc3339_timestamp() {
let json = r#""2025-03-07T12:00:00Z""#;
let Dt(dt) = serde_json::from_str(json).expect("deserialize RFC3339");
assert_eq!(dt.year(), 2025);
assert_eq!(dt.month(), time::Month::March);
assert_eq!(dt.day(), 7);
assert_eq!(dt.hour(), 12);
}
}