// 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. 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; #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] pub enum AccountStatus { #[serde(rename = "enabled")] Enabled, #[serde(rename = "disabled")] #[default] Disabled, } impl AsRef for AccountStatus { fn as_ref(&self) -> &str { match self { AccountStatus::Enabled => "enabled", AccountStatus::Disabled => "disabled", } } } impl TryFrom<&str> for AccountStatus { type Error = String; fn try_from(s: &str) -> Result { match s { "enabled" => Ok(AccountStatus::Enabled), "disabled" => Ok(AccountStatus::Disabled), _ => Err(format!("invalid account status: {s}")), } } } #[derive(Debug, Serialize, Deserialize)] pub enum UserAuthType { #[serde(rename = "builtin")] Builtin, #[serde(rename = "ldap")] Ldap, #[serde(rename = "oidc")] Oidc, } #[derive(Debug, Serialize, Deserialize)] pub struct UserAuthInfo { #[serde(rename = "type")] pub auth_type: UserAuthType, #[serde(rename = "authServer", skip_serializing_if = "Option::is_none")] pub auth_server: Option, #[serde(rename = "authServerUserID", skip_serializing_if = "Option::is_none")] pub auth_server_user_id: Option, } #[derive(Debug, Serialize, Deserialize, Default)] pub struct UserInfo { #[serde(rename = "userAuthInfo", skip_serializing_if = "Option::is_none")] pub auth_info: Option, #[serde(rename = "secretKey", skip_serializing_if = "Option::is_none")] pub secret_key: Option, #[serde(rename = "policyName", skip_serializing_if = "Option::is_none")] pub policy_name: Option, #[serde(rename = "status")] pub status: AccountStatus, #[serde(rename = "memberOf", skip_serializing_if = "Option::is_none")] pub member_of: Option>, #[serde(rename = "updatedAt", with = "time::serde::rfc3339::option")] pub updated_at: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AddOrUpdateUserReq { #[serde(rename = "secretKey")] pub secret_key: String, #[serde(rename = "policy", skip_serializing_if = "Option::is_none")] pub policy: Option, #[serde(rename = "status")] pub status: AccountStatus, } #[derive(Debug, Serialize, Deserialize)] pub struct ServiceAccountInfo { #[serde(rename = "parentUser")] pub parent_user: String, #[serde(rename = "accountStatus")] pub account_status: String, #[serde(rename = "impliedPolicy")] pub implied_policy: bool, #[serde(rename = "accessKey")] pub access_key: String, #[serde(rename = "name", skip_serializing_if = "Option::is_none")] pub name: Option, #[serde(rename = "description", skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(rename = "expiration", with = "time::serde::rfc3339::option")] pub expiration: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct ListServiceAccountsResp { #[serde(rename = "accounts")] pub accounts: Vec, } #[derive(Debug, Serialize, Deserialize, Default)] pub struct ListAccessKeysResp { #[serde(rename = "serviceAccounts", default)] pub service_accounts: Vec, #[serde(rename = "stsKeys", default)] pub sts_keys: Vec, } pub const ACCESS_KEY_LIST_USERS_ONLY: &str = "users-only"; pub const ACCESS_KEY_LIST_STS_ONLY: &str = "sts-only"; pub const ACCESS_KEY_LIST_SVCACC_ONLY: &str = "svcacc-only"; pub const ACCESS_KEY_LIST_ALL: &str = "all"; #[derive(Debug, Serialize, Deserialize)] pub struct AddServiceAccountReq { #[serde( rename = "policy", skip_serializing_if = "Option::is_none", default, deserialize_with = "deserialize_optional_policy_value" )] pub policy: Option, #[serde(rename = "targetUser", skip_serializing_if = "Option::is_none")] pub target_user: Option, #[serde(rename = "accessKey", default)] pub access_key: String, #[serde(rename = "secretKey", default)] pub secret_key: String, #[serde(rename = "name", skip_serializing_if = "Option::is_none")] pub name: Option, #[serde(rename = "description", skip_serializing_if = "Option::is_none")] pub description: Option, #[serde( rename = "expiration", skip_serializing_if = "Option::is_none", default, with = "time::serde::rfc3339::option" )] pub expiration: Option, #[serde(rename = "comment", skip_serializing_if = "Option::is_none")] pub comment: Option, } impl AddServiceAccountReq { pub fn validate(&self) -> Result<(), String> { validate_service_account_name(self.name.as_deref())?; validate_service_account_description(self.description.as_deref().or(self.comment.as_deref()))?; validate_service_account_expiration(self.expiration) } } #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct Credentials<'a> { pub access_key: &'a str, pub secret_key: &'a str, #[serde(skip_serializing_if = "Option::is_none")] pub session_token: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] #[serde(with = "time::serde::rfc3339::option")] pub expiration: Option, } #[derive(Serialize)] pub struct AddServiceAccountResp<'a> { pub credentials: Credentials<'a>, } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct InfoServiceAccountResp { pub parent_user: String, pub account_status: String, pub implied_policy: bool, #[serde(skip_serializing_if = "Option::is_none")] pub policy: Option, #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(with = "time::serde::rfc3339::option")] pub expiration: Option, } pub type TemporaryAccountInfoResp = InfoServiceAccountResp; #[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq)] pub struct LDAPSpecificAccessKeyInfo { #[serde(rename = "username", skip_serializing_if = "Option::is_none")] pub username: Option, } impl LDAPSpecificAccessKeyInfo { pub fn is_empty(&self) -> bool { self.username.is_none() } } #[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq)] pub struct OpenIDSpecificAccessKeyInfo { #[serde(rename = "configName", skip_serializing_if = "Option::is_none")] pub config_name: Option, #[serde(rename = "userID", skip_serializing_if = "Option::is_none")] pub user_id: Option, #[serde(rename = "userIDClaim", skip_serializing_if = "Option::is_none")] pub user_id_claim: Option, #[serde(rename = "displayName", skip_serializing_if = "Option::is_none")] pub display_name: Option, #[serde(rename = "displayNameClaim", skip_serializing_if = "Option::is_none")] pub display_name_claim: Option, } impl OpenIDSpecificAccessKeyInfo { pub fn is_empty(&self) -> bool { self.config_name.is_none() && self.user_id.is_none() && self.user_id_claim.is_none() && self.display_name.is_none() && self.display_name_claim.is_none() } } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct InfoAccessKeyResp { pub access_key: String, #[serde(flatten)] pub info: InfoServiceAccountResp, pub user_type: String, pub user_provider: String, #[serde(rename = "ldapSpecificInfo", skip_serializing_if = "LDAPSpecificAccessKeyInfo::is_empty")] pub ldap_specific_info: LDAPSpecificAccessKeyInfo, #[serde( rename = "openIDSpecificInfo", skip_serializing_if = "OpenIDSpecificAccessKeyInfo::is_empty" )] pub open_id_specific_info: OpenIDSpecificAccessKeyInfo, } #[derive(Debug, Serialize, Deserialize)] pub struct UpdateServiceAccountReq { #[serde( rename = "newPolicy", skip_serializing_if = "Option::is_none", default, deserialize_with = "deserialize_optional_policy_value" )] pub new_policy: Option, #[serde(rename = "newSecretKey", skip_serializing_if = "Option::is_none")] pub new_secret_key: Option, #[serde(rename = "newStatus", skip_serializing_if = "Option::is_none")] pub new_status: Option, #[serde(rename = "newName", skip_serializing_if = "Option::is_none")] pub new_name: Option, #[serde(rename = "newDescription", skip_serializing_if = "Option::is_none")] pub new_description: Option, #[serde(rename = "newExpiration", skip_serializing_if = "Option::is_none", default)] #[serde(with = "time::serde::rfc3339::option")] pub new_expiration: Option, } impl UpdateServiceAccountReq { pub fn validate(&self) -> Result<(), String> { validate_service_account_name(self.new_name.as_deref())?; validate_service_account_description(self.new_description.as_deref())?; validate_service_account_expiration(self.new_expiration) } } fn deserialize_optional_policy_value<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let value = Option::::deserialize(deserializer)?; Ok(value.map(normalize_policy_value)) } fn normalize_policy_value(value: Value) -> Value { match value { Value::String(policy) => serde_json::from_str(&policy).unwrap_or(Value::String(policy)), other => other, } } fn validate_service_account_name(name: Option<&str>) -> Result<(), String> { let Some(name) = name else { return Ok(()); }; if name.is_empty() { return Ok(()); } if name.len() > 32 { return Err("name must not be longer than 32 characters".to_string()); } let mut chars = name.chars(); let Some(first) = chars.next() else { return Ok(()); }; if !first.is_ascii_alphabetic() { return Err( "name must contain only ASCII letters, digits, underscores and hyphens and must start with a letter".to_string(), ); } if chars.any(|c| !c.is_ascii_alphanumeric() && c != '_' && c != '-') { return Err( "name must contain only ASCII letters, digits, underscores and hyphens and must start with a letter".to_string(), ); } Ok(()) } fn validate_service_account_description(description: Option<&str>) -> Result<(), String> { let Some(description) = description else { return Ok(()); }; if description.len() > 256 { return Err("description must be at most 256 bytes long".to_string()); } Ok(()) } fn validate_service_account_expiration(expiration: Option) -> Result<(), String> { let Some(expiration) = expiration else { return Ok(()); }; if expiration.unix_timestamp() == 0 { return Ok(()); } if expiration < OffsetDateTime::now_utc() { return Err("the expiration time should be in the future".to_string()); } Ok(()) } #[derive(Debug, Serialize, Deserialize, Default)] pub struct AccountInfo { pub account_name: String, pub server: BackendInfo, pub policy: serde_json::Value, // Use iam/policy::parse to parse the result, to be done by the caller. pub buckets: Vec, } #[derive(Debug, Serialize, Deserialize, Default)] pub struct BucketAccessInfo { pub name: String, pub size: u64, pub objects: u64, pub object_sizes_histogram: HashMap, pub object_versions_histogram: HashMap, pub details: Option, pub prefix_usage: HashMap, #[serde(rename = "expiration", with = "time::serde::rfc3339::option")] pub created: Option, pub access: AccountAccess, } #[derive(Debug, Serialize, Deserialize, Default)] pub struct BucketDetails { pub versioning: bool, pub versioning_suspended: bool, pub locking: bool, pub replication: bool, // pub tagging: Option, } #[derive(Debug, Serialize, Deserialize, Default)] pub struct AccountAccess { pub read: bool, pub write: bool, } /// SRSessionPolicy - represents a session policy to be replicated. #[derive(Debug, Clone)] pub struct SRSessionPolicy(Option>); impl SRSessionPolicy { pub fn new() -> Self { SRSessionPolicy(None) } pub fn from_json(json: &str) -> Result { if json == "null" { Ok(SRSessionPolicy(None)) } else { let raw_value = serde_json::from_str(json)?; Ok(SRSessionPolicy(Some(raw_value))) } } pub fn is_null(&self) -> bool { self.0.is_none() } pub fn as_str(&self) -> Option<&str> { self.0.as_ref().map(|v| v.get()) } } impl Default for SRSessionPolicy { fn default() -> Self { Self::new() } } impl PartialEq for SRSessionPolicy { fn eq(&self, other: &Self) -> bool { self.0.as_ref().map(|v| v.get()) == other.0.as_ref().map(|v| v.get()) } } impl Serialize for SRSessionPolicy { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match &self.0 { Some(raw_value) => raw_value.serialize(serializer), None => serializer.serialize_none(), } } } impl<'de> Deserialize<'de> for SRSessionPolicy { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let raw_value: Option> = Option::deserialize(deserializer)?; Ok(SRSessionPolicy(raw_value)) } } fn deserialize_vec_or_default<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { Ok(Option::>::deserialize(deserializer)?.unwrap_or_default()) } fn parse_service_account_expiration(expiration: &str) -> Result { 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(expiration: &Option, serializer: S) -> Result where S: Serializer, { time::serde::rfc3339::option::serialize(expiration, serializer) } 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 = expiration.trim(); if expiration.is_empty() { return Ok(None); } let expiration = parse_service_account_expiration(expiration).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 { pub parent: String, #[serde(rename = "accessKey")] pub access_key: String, #[serde(rename = "secretKey")] pub secret_key: String, #[serde(default, deserialize_with = "deserialize_vec_or_default")] pub groups: Vec, pub claims: HashMap, #[serde(rename = "sessionPolicy")] pub session_policy: SRSessionPolicy, pub status: String, pub name: String, pub description: String, #[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, #[serde(rename = "apiVersion", skip_serializing_if = "Option::is_none")] pub api_version: Option, } /// ImportIAMResult - represents the structure iam import response #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ImportIAMResult { /// Skipped entries while import /// This could be due to groups, policies etc missing for /// imported entries. We dont fail hard in this case and pub skipped: IAMEntities, /// Removed entries - this mostly happens for policies /// where empty might be getting imported and that's invalid pub removed: IAMEntities, /// Newly added entries pub added: IAMEntities, /// Failed entries while import. This would have details of /// failed entities with respective errors pub failed: IAMErrEntities, } /// IAMEntities - represents different IAM entities #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct IAMEntities { /// List of policy names pub policies: Vec, /// List of user names pub users: Vec, /// List of group names pub groups: Vec, /// List of Service Account names #[serde(rename = "serviceAccounts")] pub service_accounts: Vec, /// List of user policies, each entry in map represents list of policies /// applicable to the user #[serde(rename = "userPolicies")] pub user_policies: Vec>>, /// List of group policies, each entry in map represents list of policies /// applicable to the group #[serde(rename = "groupPolicies")] pub group_policies: Vec>>, /// List of STS policies, each entry in map represents list of policies /// applicable to the STS #[serde(rename = "stsPolicies")] pub sts_policies: Vec>>, } /// PolicyEntitiesResult - contains response to a policy entities query. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PolicyEntitiesResult { #[serde(rename = "timestamp", with = "time::serde::rfc3339")] pub timestamp: time::OffsetDateTime, #[serde(rename = "userMappings", skip_serializing_if = "Vec::is_empty")] pub user_mappings: Vec, #[serde(rename = "groupMappings", skip_serializing_if = "Vec::is_empty")] pub group_mappings: Vec, #[serde(rename = "policyMappings", skip_serializing_if = "Vec::is_empty")] pub policy_mappings: Vec, } impl Default for PolicyEntitiesResult { fn default() -> Self { Self { timestamp: time::OffsetDateTime::UNIX_EPOCH, user_mappings: Vec::new(), group_mappings: Vec::new(), policy_mappings: Vec::new(), } } } /// UserPolicyEntities - user -> policies mapping #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct UserPolicyEntities { pub user: String, pub policies: Vec, #[serde(rename = "memberOfMappings", skip_serializing_if = "Vec::is_empty")] pub member_of_mappings: Vec, } /// GroupPolicyEntities - group -> policies mapping #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct GroupPolicyEntities { pub group: String, pub policies: Vec, } /// PolicyEntities - policy -> user+group mapping #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct PolicyEntities { pub policy: String, pub users: Vec, pub groups: Vec, } /// IAMErrEntities - represents errored out IAM entries while import with error #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct IAMErrEntities { /// List of errored out policies with errors pub policies: Vec, /// List of errored out users with errors pub users: Vec, /// List of errored out groups with errors pub groups: Vec, /// List of errored out service accounts with errors #[serde(rename = "serviceAccounts")] pub service_accounts: Vec, /// List of errored out user policies with errors #[serde(rename = "userPolicies")] pub user_policies: Vec, /// List of errored out group policies with errors #[serde(rename = "groupPolicies")] pub group_policies: Vec, /// List of errored out STS policies with errors #[serde(rename = "stsPolicies")] pub sts_policies: Vec, } /// IAMErrEntity - represents an errored IAM entity with error details #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IAMErrEntity { pub name: String, pub error: String, } /// IAMErrPolicyEntity - represents an errored policy entity with error details #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IAMErrPolicyEntity { pub name: String, pub policies: Vec, pub error: String, } #[cfg(test)] mod tests { use super::*; use serde_json; use time::OffsetDateTime; use time::macros::datetime; #[test] fn test_account_status_default() { let status = AccountStatus::default(); assert_eq!(status, AccountStatus::Disabled); } #[test] fn test_account_status_as_ref() { assert_eq!(AccountStatus::Enabled.as_ref(), "enabled"); assert_eq!(AccountStatus::Disabled.as_ref(), "disabled"); } #[test] fn test_account_status_try_from_valid() { assert_eq!(AccountStatus::try_from("enabled").unwrap(), AccountStatus::Enabled); assert_eq!(AccountStatus::try_from("disabled").unwrap(), AccountStatus::Disabled); } #[test] fn test_account_status_try_from_invalid() { let result = AccountStatus::try_from("invalid"); assert!(result.is_err()); assert!(result.unwrap_err().contains("invalid account status")); } #[test] fn test_account_status_serialization() { let enabled = AccountStatus::Enabled; let disabled = AccountStatus::Disabled; let enabled_json = serde_json::to_string(&enabled).unwrap(); let disabled_json = serde_json::to_string(&disabled).unwrap(); assert_eq!(enabled_json, "\"enabled\""); assert_eq!(disabled_json, "\"disabled\""); } #[test] fn test_account_status_deserialization() { let enabled: AccountStatus = serde_json::from_str("\"enabled\"").unwrap(); let disabled: AccountStatus = serde_json::from_str("\"disabled\"").unwrap(); assert_eq!(enabled, AccountStatus::Enabled); assert_eq!(disabled, AccountStatus::Disabled); } #[test] fn test_user_auth_type_serialization() { let builtin = UserAuthType::Builtin; let ldap = UserAuthType::Ldap; let builtin_json = serde_json::to_string(&builtin).unwrap(); let ldap_json = serde_json::to_string(&ldap).unwrap(); assert_eq!(builtin_json, "\"builtin\""); assert_eq!(ldap_json, "\"ldap\""); } #[test] fn test_user_auth_info_creation() { let auth_info = UserAuthInfo { auth_type: UserAuthType::Ldap, auth_server: Some("ldap.example.com".to_string()), auth_server_user_id: Some("user123".to_string()), }; assert!(matches!(auth_info.auth_type, UserAuthType::Ldap)); assert_eq!(auth_info.auth_server.unwrap(), "ldap.example.com"); assert_eq!(auth_info.auth_server_user_id.unwrap(), "user123"); } #[test] fn test_user_auth_info_serialization() { let auth_info = UserAuthInfo { auth_type: UserAuthType::Builtin, auth_server: None, auth_server_user_id: None, }; let json = serde_json::to_string(&auth_info).unwrap(); assert!(json.contains("builtin")); assert!(!json.contains("authServer"), "None fields should be skipped"); } #[test] fn test_user_info_default() { let user_info = UserInfo::default(); assert!(user_info.auth_info.is_none()); assert!(user_info.secret_key.is_none()); assert!(user_info.policy_name.is_none()); assert_eq!(user_info.status, AccountStatus::Disabled); assert!(user_info.member_of.is_none()); assert!(user_info.updated_at.is_none()); } #[test] fn test_user_info_with_values() { let now = OffsetDateTime::now_utc(); let user_info = UserInfo { auth_info: Some(UserAuthInfo { auth_type: UserAuthType::Builtin, auth_server: None, auth_server_user_id: None, }), secret_key: Some("secret123".to_string()), policy_name: Some("ReadOnlyAccess".to_string()), status: AccountStatus::Enabled, member_of: Some(vec!["group1".to_string(), "group2".to_string()]), updated_at: Some(now), }; assert!(user_info.auth_info.is_some()); assert_eq!(user_info.secret_key.unwrap(), "secret123"); assert_eq!(user_info.policy_name.unwrap(), "ReadOnlyAccess"); assert_eq!(user_info.status, AccountStatus::Enabled); assert_eq!(user_info.member_of.unwrap().len(), 2); assert!(user_info.updated_at.is_some()); } #[test] fn test_add_or_update_user_req_creation() { let req = AddOrUpdateUserReq { secret_key: "newsecret".to_string(), policy: Some("FullAccess".to_string()), status: AccountStatus::Enabled, }; assert_eq!(req.secret_key, "newsecret"); assert_eq!(req.policy.unwrap(), "FullAccess"); assert_eq!(req.status, AccountStatus::Enabled); } #[test] fn test_service_account_info_creation() { let now = OffsetDateTime::now_utc(); let service_account = ServiceAccountInfo { parent_user: "admin".to_string(), account_status: "enabled".to_string(), implied_policy: true, access_key: "AKIAIOSFODNN7EXAMPLE".to_string(), name: Some("test-service".to_string()), description: Some("Test service account".to_string()), expiration: Some(now), }; assert_eq!(service_account.parent_user, "admin"); assert_eq!(service_account.account_status, "enabled"); assert!(service_account.implied_policy); assert_eq!(service_account.access_key, "AKIAIOSFODNN7EXAMPLE"); assert_eq!(service_account.name.unwrap(), "test-service"); assert!(service_account.expiration.is_some()); } #[test] fn test_list_service_accounts_resp_creation() { let resp = ListServiceAccountsResp { accounts: vec![ ServiceAccountInfo { parent_user: "user1".to_string(), account_status: "enabled".to_string(), implied_policy: false, access_key: "KEY1".to_string(), name: Some("service1".to_string()), description: None, expiration: None, }, ServiceAccountInfo { parent_user: "user2".to_string(), account_status: "disabled".to_string(), implied_policy: true, access_key: "KEY2".to_string(), name: Some("service2".to_string()), description: Some("Second service".to_string()), expiration: None, }, ], }; assert_eq!(resp.accounts.len(), 2); assert_eq!(resp.accounts[0].parent_user, "user1"); assert_eq!(resp.accounts[1].account_status, "disabled"); } #[test] fn test_add_service_account_req_validate_success() { let req = AddServiceAccountReq { policy: Some(serde_json::json!({"Version": "2012-10-17"})), target_user: Some("testuser".to_string()), access_key: "AKIAIOSFODNN7EXAMPLE".to_string(), secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string(), name: Some("test-service".to_string()), description: Some("Test service account".to_string()), expiration: None, comment: None, }; let result = req.validate(); assert!(result.is_ok()); } #[test] fn test_add_service_account_req_validate_allows_generated_credentials() { let req = AddServiceAccountReq { policy: None, target_user: None, access_key: "".to_string(), secret_key: "".to_string(), name: None, description: None, expiration: None, comment: None, }; assert!(req.validate().is_ok()); } #[test] fn test_add_service_account_req_deserializes_stringified_policy_json() { let req: AddServiceAccountReq = serde_json::from_str( r#"{ "policy":"{\"Version\":\"2012-10-17\",\"Statement\":[]}", "accessKey":"AKIAIOSFODNN7EXAMPLE", "secretKey":"secret" }"#, ) .unwrap(); assert_eq!(req.policy, Some(serde_json::json!({"Version":"2012-10-17","Statement":[]}))); } #[test] fn test_add_service_account_req_allows_missing_policy_field() { let req: AddServiceAccountReq = serde_json::from_str( r#"{ "accessKey":"AKIAIOSFODNN7EXAMPLE", "secretKey":"secret" }"#, ) .unwrap(); assert_eq!(req.policy, None); } #[test] fn test_add_service_account_req_validate_invalid_name() { let req = AddServiceAccountReq { policy: None, target_user: None, access_key: "AKIAIOSFODNN7EXAMPLE".to_string(), secret_key: "secret".to_string(), name: Some("1invalid".to_string()), description: None, expiration: None, comment: None, }; let result = req.validate(); assert!(result.is_err()); assert!(result.unwrap_err().contains("must start with a letter")); } #[test] fn test_add_service_account_req_validate_rejects_long_description() { let req = AddServiceAccountReq { policy: None, target_user: None, access_key: "AKIAIOSFODNN7EXAMPLE".to_string(), secret_key: "secret".to_string(), name: Some("test".to_string()), description: Some("a".repeat(257)), expiration: None, comment: None, }; let result = req.validate(); assert!(result.is_err()); assert!(result.unwrap_err().contains("at most 256 bytes")); } #[test] fn test_credentials_serialization() { let now = OffsetDateTime::now_utc(); let credentials = Credentials { access_key: "AKIAIOSFODNN7EXAMPLE", secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", session_token: Some("session123"), expiration: Some(now), }; let json = serde_json::to_string(&credentials).unwrap(); assert!(json.contains("AKIAIOSFODNN7EXAMPLE")); assert!(json.contains("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")); assert!(json.contains("session123")); } #[test] fn test_credentials_without_optional_fields() { let credentials = Credentials { access_key: "AKIAIOSFODNN7EXAMPLE", secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", session_token: None, expiration: None, }; let json = serde_json::to_string(&credentials).unwrap(); assert!(json.contains("AKIAIOSFODNN7EXAMPLE")); assert!(!json.contains("sessionToken"), "None fields should be skipped"); assert!(!json.contains("expiration"), "None fields should be skipped"); } #[test] fn test_add_service_account_resp_creation() { let credentials = Credentials { access_key: "AKIAIOSFODNN7EXAMPLE", secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", session_token: None, expiration: None, }; let resp = AddServiceAccountResp { credentials }; assert_eq!(resp.credentials.access_key, "AKIAIOSFODNN7EXAMPLE"); assert_eq!(resp.credentials.secret_key, "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"); } #[test] fn test_info_service_account_resp_creation() { let now = OffsetDateTime::now_utc(); let resp = InfoServiceAccountResp { parent_user: "admin".to_string(), account_status: "enabled".to_string(), implied_policy: true, policy: Some("ReadOnlyAccess".to_string()), name: Some("test-service".to_string()), description: Some("Test service account".to_string()), expiration: Some(now), }; assert_eq!(resp.parent_user, "admin"); assert_eq!(resp.account_status, "enabled"); assert!(resp.implied_policy); assert_eq!(resp.policy.unwrap(), "ReadOnlyAccess"); assert_eq!(resp.name.unwrap(), "test-service"); assert!(resp.expiration.is_some()); } #[test] fn test_update_service_account_req_validate() { let req = UpdateServiceAccountReq { new_policy: Some(serde_json::json!({"Version": "2012-10-17"})), new_secret_key: Some("newsecret".to_string()), new_status: Some("enabled".to_string()), new_name: Some("updated-service".to_string()), new_description: Some("Updated description".to_string()), new_expiration: None, }; let result = req.validate(); assert!(result.is_ok()); } #[test] fn test_update_service_account_req_deserializes_stringified_policy_json() { let req: UpdateServiceAccountReq = serde_json::from_str( r#"{ "newPolicy":"{\"Version\":\"2012-10-17\",\"Statement\":[]}" }"#, ) .unwrap(); assert_eq!(req.new_policy, Some(serde_json::json!({"Version":"2012-10-17","Statement":[]}))); } #[test] fn test_update_service_account_req_allows_missing_policy_field() { let req: UpdateServiceAccountReq = serde_json::from_str(r#"{}"#).unwrap(); assert_eq!(req.new_policy, None); } #[test] fn test_account_info_creation() { use crate::BackendInfo; let account_info = AccountInfo { account_name: "testuser".to_string(), server: BackendInfo::default(), policy: serde_json::json!({"Version": "2012-10-17"}), buckets: vec![], }; assert_eq!(account_info.account_name, "testuser"); assert!(account_info.buckets.is_empty()); assert!(account_info.policy.is_object()); } #[test] fn test_bucket_access_info_creation() { let now = OffsetDateTime::now_utc(); let mut sizes_histogram = HashMap::new(); sizes_histogram.insert("small".to_string(), 100); sizes_histogram.insert("large".to_string(), 50); let mut versions_histogram = HashMap::new(); versions_histogram.insert("v1".to_string(), 80); versions_histogram.insert("v2".to_string(), 70); let mut prefix_usage = HashMap::new(); prefix_usage.insert("logs/".to_string(), 1000000); prefix_usage.insert("data/".to_string(), 5000000); let bucket_info = BucketAccessInfo { name: "test-bucket".to_string(), size: 6000000, objects: 150, object_sizes_histogram: sizes_histogram, object_versions_histogram: versions_histogram, details: Some(BucketDetails { versioning: true, versioning_suspended: false, locking: true, replication: false, }), prefix_usage, created: Some(now), access: AccountAccess { read: true, write: false, }, }; assert_eq!(bucket_info.name, "test-bucket"); assert_eq!(bucket_info.size, 6000000); assert_eq!(bucket_info.objects, 150); assert_eq!(bucket_info.object_sizes_histogram.len(), 2); assert_eq!(bucket_info.object_versions_histogram.len(), 2); assert!(bucket_info.details.is_some()); assert_eq!(bucket_info.prefix_usage.len(), 2); assert!(bucket_info.created.is_some()); assert!(bucket_info.access.read); assert!(!bucket_info.access.write); } #[test] fn test_bucket_details_creation() { let details = BucketDetails { versioning: true, versioning_suspended: false, locking: true, replication: true, }; assert!(details.versioning); assert!(!details.versioning_suspended); assert!(details.locking); assert!(details.replication); } #[test] fn test_account_access_creation() { let read_only = AccountAccess { read: true, write: false, }; let full_access = AccountAccess { read: true, write: true }; let no_access = AccountAccess { read: false, write: false, }; assert!(read_only.read && !read_only.write); assert!(full_access.read && full_access.write); assert!(!no_access.read && !no_access.write); } #[test] fn test_serialization_deserialization_roundtrip() { let now = OffsetDateTime::now_utc().replace_nanosecond(0).unwrap(); let user_info = UserInfo { auth_info: Some(UserAuthInfo { auth_type: UserAuthType::Ldap, auth_server: Some("ldap.example.com".to_string()), auth_server_user_id: Some("user123".to_string()), }), secret_key: Some("secret123".to_string()), policy_name: Some("ReadOnlyAccess".to_string()), status: AccountStatus::Enabled, member_of: Some(vec!["group1".to_string()]), updated_at: Some(now), }; let json = serde_json::to_string(&user_info).unwrap(); let deserialized: UserInfo = serde_json::from_str(&json).unwrap(); assert!(json.contains("\"updatedAt\":\"")); assert!(json.contains('T')); assert_eq!(deserialized.secret_key.unwrap(), "secret123"); assert_eq!(deserialized.policy_name.unwrap(), "ReadOnlyAccess"); assert_eq!(deserialized.status, AccountStatus::Enabled); assert_eq!(deserialized.member_of.unwrap().len(), 1); assert_eq!(deserialized.updated_at, Some(now)); } #[test] fn test_debug_format_all_structures() { let account_status = AccountStatus::Enabled; let user_auth_type = UserAuthType::Builtin; let user_info = UserInfo::default(); let service_account = ServiceAccountInfo { parent_user: "test".to_string(), account_status: "enabled".to_string(), implied_policy: false, access_key: "key".to_string(), name: None, description: None, expiration: None, }; // Test that all structures can be formatted with Debug assert!(!format!("{account_status:?}").is_empty()); assert!(!format!("{user_auth_type:?}").is_empty()); assert!(!format!("{user_info:?}").is_empty()); assert!(!format!("{service_account:?}").is_empty()); } #[test] fn test_memory_efficiency() { // Test that structures don't use excessive memory assert!(std::mem::size_of::() < 100); assert!(std::mem::size_of::() < 100); assert!(std::mem::size_of::() < 2000); assert!(std::mem::size_of::() < 2000); assert!(std::mem::size_of::() < 100); } #[test] fn test_edge_cases() { // Test empty strings and edge cases let req = AddServiceAccountReq { policy: Some(serde_json::Value::Null), target_user: Some("".to_string()), access_key: "valid_key".to_string(), secret_key: "valid_secret".to_string(), name: Some("valid_name".to_string()), description: Some("".to_string()), expiration: None, comment: None, }; // Should still validate successfully with empty optional strings assert!(req.validate().is_ok()); // Test very long strings let long_string = "a".repeat(1000); let long_req = AddServiceAccountReq { policy: Some(serde_json::json!({"Statement": [long_string]})), target_user: Some(long_string.clone()), access_key: long_string.clone(), secret_key: long_string, name: Some("valid_name".to_string()), description: Some("valid description".to_string()), expiration: None, comment: None, }; assert!(long_req.validate().is_ok()); } #[test] fn test_sr_svc_acc_create_deserialize_empty_expiration_as_none() { let payload = r#"{ "parent": "useralpha", "accessKey": "svcalpha", "secretKey": "svcAlphaSecret123", "groups": [], "claims": {}, "sessionPolicy": null, "status": "on", "name": "uploaderKey", "description": "alpha upload key", "expiration": " " }"#; 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))); } }