fix(user): service account expiration handling with RFC3339 (#3078)

fix(user): enhance service account expiration handling with RFC3339 support

Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
Sergei Z.
2026-05-25 09:27:25 +05:00
committed by GitHub
parent f1207a9e28
commit 64feca3550
+65 -1
View File
@@ -500,6 +500,22 @@ where
Ok(Option::<Vec<String>>::deserialize(deserializer)?.unwrap_or_default())
}
fn parse_service_account_expiration(expiration: &str) -> Result<OffsetDateTime, time::Error> {
const LEGACY_SERVICE_ACCOUNT_EXPIRATION_FORMAT: &[time::format_description::BorrowedFormatItem<'_>] = time::macros::format_description!(
"[year]-[month]-[day] [hour]:[minute]:[second].[subsecond] [offset_hour sign:mandatory]:[offset_minute]:[offset_second]"
);
OffsetDateTime::parse(expiration, &Rfc3339)
.or_else(|_| OffsetDateTime::parse(expiration, LEGACY_SERVICE_ACCOUNT_EXPIRATION_FORMAT).map_err(Into::into))
}
fn serialize_optional_service_account_expiration<S>(expiration: &Option<OffsetDateTime>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
time::serde::rfc3339::option::serialize(expiration, serializer)
}
fn deserialize_optional_service_account_expiration<'de, D>(deserializer: D) -> Result<Option<OffsetDateTime>, D::Error>
where
D: Deserializer<'de>,
@@ -513,7 +529,7 @@ where
return Ok(None);
}
let expiration = OffsetDateTime::parse(expiration, &Rfc3339).map_err(D::Error::custom)?;
let expiration = parse_service_account_expiration(expiration).map_err(D::Error::custom)?;
if expiration.unix_timestamp() == 0 {
return Ok(None);
}
@@ -549,6 +565,7 @@ pub struct SRSvcAccCreate {
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_optional_service_account_expiration",
deserialize_with = "deserialize_optional_service_account_expiration"
)]
pub expiration: Option<OffsetDateTime>,
@@ -706,6 +723,7 @@ mod tests {
use super::*;
use serde_json;
use time::OffsetDateTime;
use time::macros::datetime;
#[test]
fn test_account_status_default() {
@@ -1295,4 +1313,50 @@ mod tests {
let svc: SRSvcAccCreate = serde_json::from_str(payload).unwrap();
assert!(svc.expiration.is_none());
}
#[test]
fn test_sr_svc_acc_create_serializes_expiration_as_rfc3339() {
let svc = SRSvcAccCreate {
parent: "useralpha".to_string(),
access_key: "svcalpha".to_string(),
secret_key: "svcAlphaSecret123".to_string(),
groups: Vec::new(),
claims: HashMap::new(),
session_policy: SRSessionPolicy::default(),
status: "on".to_string(),
name: "uploaderKey".to_string(),
description: "alpha upload key".to_string(),
expiration: Some(datetime!(9999-01-01 00:00:00 UTC)),
api_version: None,
};
let json = serde_json::to_string(&svc).unwrap();
assert!(
json.contains(r#""expiration":"9999-01-01T00:00:00Z""#),
"service account export expiration should be RFC3339; got: {json}"
);
assert!(!json.contains("+00:00:00"), "export must not use the legacy offset format: {json}");
let decoded: SRSvcAccCreate = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.expiration, svc.expiration);
}
#[test]
fn test_sr_svc_acc_create_deserializes_legacy_exported_expiration() {
let payload = r#"{
"parent": "useralpha",
"accessKey": "svcalpha",
"secretKey": "svcAlphaSecret123",
"groups": [],
"claims": {},
"sessionPolicy": null,
"status": "on",
"name": "uploaderKey",
"description": "alpha upload key",
"expiration": "9999-01-01 00:00:00.0 +00:00:00"
}"#;
let svc: SRSvcAccCreate = serde_json::from_str(payload).unwrap();
assert_eq!(svc.expiration, Some(datetime!(9999-01-01 00:00:00 UTC)));
}
}