mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-15 01:23:12 +00:00
feat: improve legacy metadata and admin compatibility (#2202)
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user