diff --git a/crates/madmin/src/user.rs b/crates/madmin/src/user.rs index e98c2c23b..b9be66840 100644 --- a/crates/madmin/src/user.rs +++ b/crates/madmin/src/user.rs @@ -12,11 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeError}; use serde_json::Value; use serde_json::value::RawValue; use std::collections::HashMap; use time::OffsetDateTime; +use time::format_description::well_known::Rfc3339; use crate::BackendInfo; @@ -492,6 +493,30 @@ impl<'de> Deserialize<'de> for SRSessionPolicy { } } +fn deserialize_vec_or_default<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + Ok(Option::>::deserialize(deserializer)?.unwrap_or_default()) +} + +fn deserialize_optional_service_account_expiration<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let expiration = Option::::deserialize(deserializer)?; + let Some(expiration) = expiration else { + return Ok(None); + }; + + let expiration = OffsetDateTime::parse(&expiration, &Rfc3339).map_err(D::Error::custom)?; + if expiration.unix_timestamp() == 0 { + return Ok(None); + } + + Ok(Some(expiration)) +} + /// SRSvcAccCreate - create operation #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SRSvcAccCreate { @@ -503,6 +528,7 @@ pub struct SRSvcAccCreate { #[serde(rename = "secretKey")] pub secret_key: String, + #[serde(default, deserialize_with = "deserialize_vec_or_default")] pub groups: Vec, pub claims: HashMap, @@ -516,7 +542,11 @@ pub struct SRSvcAccCreate { pub description: String, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_optional_service_account_expiration" + )] pub expiration: Option, #[serde(rename = "apiVersion", skip_serializing_if = "Option::is_none")] diff --git a/rustfs/src/admin/handlers/user.rs b/rustfs/src/admin/handlers/user.rs index d05b8a3ec..283236ea9 100644 --- a/rustfs/src/admin/handlers/user.rs +++ b/rustfs/src/admin/handlers/user.rs @@ -1120,6 +1120,7 @@ mod tests { }; use rustfs_credentials::{Credentials, IAM_POLICY_CLAIM_NAME_SA}; use rustfs_iam::error::Error as IamError; + use rustfs_madmin::user::SRSvcAccCreate; use serde_json::Value; use std::collections::HashMap; @@ -1205,6 +1206,57 @@ mod tests { assert!(imported_service_account_status("unknown").is_none()); } + #[test] + fn test_service_account_import_accepts_null_groups_and_epoch_expiration() { + let payload = r#"{ + "svcalpha": { + "parent": "useralpha", + "accessKey": "svcalpha", + "secretKey": "svcAlphaSecret123", + "groups": null, + "claims": { + "accessKey": "svcalpha", + "parent": "useralpha", + "sa-policy": "inherited-policy" + }, + "sessionPolicy": null, + "status": "on", + "name": "uploaderKey", + "description": "alpha upload key", + "expiration": "1970-01-01T00:00:00Z" + } + }"#; + + let svc_accts: HashMap = serde_json::from_str(payload).unwrap(); + let svc = svc_accts.get("svcalpha").unwrap(); + + assert!(svc.groups.is_empty()); + assert!(svc.expiration.is_none()); + } + + #[test] + fn test_service_account_import_preserves_non_zero_expiration() { + let payload = r#"{ + "svcalpha": { + "parent": "useralpha", + "accessKey": "svcalpha", + "secretKey": "svcAlphaSecret123", + "groups": [], + "claims": {}, + "sessionPolicy": null, + "status": "on", + "name": "uploaderKey", + "description": "alpha upload key", + "expiration": "2030-01-02T03:04:05Z" + } + }"#; + + let svc_accts: HashMap = serde_json::from_str(payload).unwrap(); + let svc = svc_accts.get("svcalpha").unwrap(); + + assert_eq!(svc.expiration.map(|expiration| expiration.unix_timestamp()), Some(1893553445)); + } + #[test] fn test_group_policy_mappings_use_regular_user_type() { assert_eq!(GROUP_POLICY_MAPPING_USER_TYPE, rustfs_iam::store::UserType::Reg);