mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-18 10:43:15 +00:00
feat: improve legacy metadata and admin compatibility (#2202)
This commit is contained in:
@@ -13,10 +13,11 @@
|
||||
// limitations under the License.
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
#[derive(Debug, Serialize, Default, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum GroupStatus {
|
||||
#[default]
|
||||
@@ -24,6 +25,20 @@ pub enum GroupStatus {
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for GroupStatus {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = String::deserialize(deserializer)?;
|
||||
match value.as_str() {
|
||||
"" | "enabled" => Ok(Self::Enabled),
|
||||
"disabled" => Ok(Self::Disabled),
|
||||
_ => Err(serde::de::Error::unknown_variant(&value, &["enabled", "disabled"])),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct GroupAddRemove {
|
||||
pub group: String,
|
||||
@@ -40,6 +55,40 @@ pub struct GroupDesc {
|
||||
pub status: String,
|
||||
pub members: Vec<String>,
|
||||
pub policy: String,
|
||||
#[serde(rename = "updatedAt", skip_serializing_if = "Option::is_none")]
|
||||
#[serde(
|
||||
rename = "updatedAt",
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "time::serde::rfc3339::option"
|
||||
)]
|
||||
pub updated_at: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn group_desc_updated_at_serializes_as_rfc3339() {
|
||||
let now = OffsetDateTime::now_utc().replace_nanosecond(0).unwrap();
|
||||
let group = GroupDesc {
|
||||
name: "group-a".to_string(),
|
||||
status: "enabled".to_string(),
|
||||
members: vec!["user-a".to_string()],
|
||||
policy: "readwrite".to_string(),
|
||||
updated_at: Some(now),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&group).unwrap();
|
||||
let decoded: GroupDesc = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert!(json.contains("\"updatedAt\":\""));
|
||||
assert!(json.contains('T'));
|
||||
assert_eq!(decoded.updated_at, Some(now));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_status_accepts_empty_string_as_enabled() {
|
||||
let status: GroupStatus = serde_json::from_str(r#""""#).unwrap();
|
||||
assert_eq!(status, GroupStatus::Enabled);
|
||||
}
|
||||
}
|
||||
|
||||
+325
-71
@@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::Value;
|
||||
use serde_json::value::RawValue;
|
||||
use std::collections::HashMap;
|
||||
use time::OffsetDateTime;
|
||||
@@ -88,7 +89,7 @@ pub struct UserInfo {
|
||||
#[serde(rename = "memberOf", skip_serializing_if = "Option::is_none")]
|
||||
pub member_of: Option<Vec<String>>,
|
||||
|
||||
#[serde(rename = "updatedAt")]
|
||||
#[serde(rename = "updatedAt", with = "time::serde::rfc3339::option")]
|
||||
pub updated_at: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
@@ -134,47 +135,61 @@ pub struct ListServiceAccountsResp {
|
||||
pub accounts: Vec<ServiceAccountInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct ListAccessKeysResp {
|
||||
#[serde(rename = "serviceAccounts", default)]
|
||||
pub service_accounts: Vec<ServiceAccountInfo>,
|
||||
#[serde(rename = "stsKeys", default)]
|
||||
pub sts_keys: Vec<ServiceAccountInfo>,
|
||||
}
|
||||
|
||||
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")]
|
||||
pub policy: Option<String>,
|
||||
#[serde(
|
||||
rename = "policy",
|
||||
skip_serializing_if = "Option::is_none",
|
||||
default,
|
||||
deserialize_with = "deserialize_optional_policy_value"
|
||||
)]
|
||||
pub policy: Option<Value>,
|
||||
|
||||
#[serde(rename = "targetUser", skip_serializing_if = "Option::is_none")]
|
||||
pub target_user: Option<String>,
|
||||
|
||||
#[serde(rename = "accessKey")]
|
||||
#[serde(rename = "accessKey", default)]
|
||||
pub access_key: String,
|
||||
|
||||
#[serde(rename = "secretKey")]
|
||||
#[serde(rename = "secretKey", default)]
|
||||
pub secret_key: String,
|
||||
|
||||
#[serde(rename = "name")]
|
||||
#[serde(rename = "name", skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
|
||||
#[serde(rename = "description", skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
|
||||
#[serde(rename = "expiration", with = "time::serde::rfc3339::option")]
|
||||
#[serde(
|
||||
rename = "expiration",
|
||||
skip_serializing_if = "Option::is_none",
|
||||
default,
|
||||
with = "time::serde::rfc3339::option"
|
||||
)]
|
||||
pub expiration: Option<OffsetDateTime>,
|
||||
|
||||
#[serde(rename = "comment", skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
impl AddServiceAccountReq {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.access_key.is_empty() {
|
||||
return Err("accessKey is empty".to_string());
|
||||
}
|
||||
|
||||
if self.secret_key.is_empty() {
|
||||
return Err("secretKey is empty".to_string());
|
||||
}
|
||||
|
||||
if self.name.is_none() {
|
||||
return Err("name is empty".to_string());
|
||||
}
|
||||
|
||||
// TODO: validate
|
||||
|
||||
Ok(())
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +210,7 @@ pub struct AddServiceAccountResp<'a> {
|
||||
pub credentials: Credentials<'a>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InfoServiceAccountResp {
|
||||
pub parent_user: String,
|
||||
@@ -213,10 +228,70 @@ pub struct InfoServiceAccountResp {
|
||||
pub expiration: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
#[serde(rename = "userID", skip_serializing_if = "Option::is_none")]
|
||||
pub user_id: Option<String>,
|
||||
#[serde(rename = "userIDClaim", skip_serializing_if = "Option::is_none")]
|
||||
pub user_id_claim: Option<String>,
|
||||
#[serde(rename = "displayName", skip_serializing_if = "Option::is_none")]
|
||||
pub display_name: Option<String>,
|
||||
#[serde(rename = "displayNameClaim", skip_serializing_if = "Option::is_none")]
|
||||
pub display_name_claim: Option<String>,
|
||||
}
|
||||
|
||||
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")]
|
||||
pub new_policy: Option<String>,
|
||||
#[serde(
|
||||
rename = "newPolicy",
|
||||
skip_serializing_if = "Option::is_none",
|
||||
default,
|
||||
deserialize_with = "deserialize_optional_policy_value"
|
||||
)]
|
||||
pub new_policy: Option<Value>,
|
||||
|
||||
#[serde(rename = "newSecretKey", skip_serializing_if = "Option::is_none")]
|
||||
pub new_secret_key: Option<String>,
|
||||
@@ -230,18 +305,95 @@ pub struct UpdateServiceAccountReq {
|
||||
#[serde(rename = "newDescription", skip_serializing_if = "Option::is_none")]
|
||||
pub new_description: Option<String>,
|
||||
|
||||
#[serde(rename = "newExpiration", skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "newExpiration", skip_serializing_if = "Option::is_none", default)]
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub new_expiration: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
impl UpdateServiceAccountReq {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
// TODO: validate
|
||||
Ok(())
|
||||
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<Option<Value>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = Option::<Value>::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<OffsetDateTime>) -> 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,
|
||||
@@ -423,6 +575,54 @@ pub struct IAMEntities {
|
||||
pub sts_policies: Vec<HashMap<String, Vec<String>>>,
|
||||
}
|
||||
|
||||
/// 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<UserPolicyEntities>,
|
||||
#[serde(rename = "groupMappings", skip_serializing_if = "Vec::is_empty")]
|
||||
pub group_mappings: Vec<GroupPolicyEntities>,
|
||||
#[serde(rename = "policyMappings", skip_serializing_if = "Vec::is_empty")]
|
||||
pub policy_mappings: Vec<PolicyEntities>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
#[serde(rename = "memberOfMappings", skip_serializing_if = "Vec::is_empty")]
|
||||
pub member_of_mappings: Vec<GroupPolicyEntities>,
|
||||
}
|
||||
|
||||
/// GroupPolicyEntities - group -> policies mapping
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GroupPolicyEntities {
|
||||
pub group: String,
|
||||
pub policies: Vec<String>,
|
||||
}
|
||||
|
||||
/// PolicyEntities - policy -> user+group mapping
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PolicyEntities {
|
||||
pub policy: String,
|
||||
pub users: Vec<String>,
|
||||
pub groups: Vec<String>,
|
||||
}
|
||||
|
||||
/// IAMErrEntities - represents errored out IAM entries while import with error
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct IAMErrEntities {
|
||||
@@ -659,13 +859,14 @@ mod tests {
|
||||
#[test]
|
||||
fn test_add_service_account_req_validate_success() {
|
||||
let req = AddServiceAccountReq {
|
||||
policy: Some("ReadOnlyAccess".to_string()),
|
||||
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();
|
||||
@@ -673,54 +874,82 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_service_account_req_validate_empty_access_key() {
|
||||
fn test_add_service_account_req_validate_allows_generated_credentials() {
|
||||
let req = AddServiceAccountReq {
|
||||
policy: None,
|
||||
target_user: None,
|
||||
access_key: "".to_string(),
|
||||
secret_key: "secret".to_string(),
|
||||
name: Some("test".to_string()),
|
||||
description: None,
|
||||
expiration: None,
|
||||
};
|
||||
|
||||
let result = req.validate();
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("accessKey is empty"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_service_account_req_validate_empty_secret_key() {
|
||||
let req = AddServiceAccountReq {
|
||||
policy: None,
|
||||
target_user: None,
|
||||
access_key: "AKIAIOSFODNN7EXAMPLE".to_string(),
|
||||
secret_key: "".to_string(),
|
||||
name: Some("test".to_string()),
|
||||
description: None,
|
||||
expiration: None,
|
||||
};
|
||||
|
||||
let result = req.validate();
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("secretKey is empty"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_service_account_req_validate_empty_name() {
|
||||
let req = AddServiceAccountReq {
|
||||
policy: None,
|
||||
target_user: None,
|
||||
access_key: "AKIAIOSFODNN7EXAMPLE".to_string(),
|
||||
secret_key: "secret".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("name is empty"));
|
||||
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]
|
||||
@@ -793,7 +1022,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_update_service_account_req_validate() {
|
||||
let req = UpdateServiceAccountReq {
|
||||
new_policy: Some("FullAccess".to_string()),
|
||||
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()),
|
||||
@@ -805,6 +1034,25 @@ mod tests {
|
||||
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;
|
||||
@@ -904,6 +1152,7 @@ mod tests {
|
||||
|
||||
#[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,
|
||||
@@ -914,16 +1163,19 @@ mod tests {
|
||||
policy_name: Some("ReadOnlyAccess".to_string()),
|
||||
status: AccountStatus::Enabled,
|
||||
member_of: Some(vec!["group1".to_string()]),
|
||||
updated_at: None,
|
||||
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]
|
||||
@@ -962,13 +1214,14 @@ mod tests {
|
||||
fn test_edge_cases() {
|
||||
// Test empty strings and edge cases
|
||||
let req = AddServiceAccountReq {
|
||||
policy: Some("".to_string()),
|
||||
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
|
||||
@@ -977,13 +1230,14 @@ mod tests {
|
||||
// Test very long strings
|
||||
let long_string = "a".repeat(1000);
|
||||
let long_req = AddServiceAccountReq {
|
||||
policy: Some(long_string.clone()),
|
||||
policy: Some(serde_json::json!({"Statement": [long_string.clone()]})),
|
||||
target_user: Some(long_string.clone()),
|
||||
access_key: long_string.clone(),
|
||||
secret_key: long_string.clone(),
|
||||
name: Some(long_string.clone()),
|
||||
description: Some(long_string),
|
||||
name: Some("valid_name".to_string()),
|
||||
description: Some("valid description".to_string()),
|
||||
expiration: None,
|
||||
comment: None,
|
||||
};
|
||||
|
||||
assert!(long_req.validate().is_ok());
|
||||
|
||||
Reference in New Issue
Block a user