Skip to main content

rustfs_madmin/
user.rs

1// Copyright 2024 RustFS Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use serde::{Deserialize, Deserializer, Serialize, Serializer};
16use serde_json::value::RawValue;
17use std::collections::HashMap;
18use time::OffsetDateTime;
19
20use crate::BackendInfo;
21
22#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq)]
23pub enum AccountStatus {
24    #[serde(rename = "enabled")]
25    Enabled,
26    #[serde(rename = "disabled")]
27    #[default]
28    Disabled,
29}
30
31impl AsRef<str> for AccountStatus {
32    fn as_ref(&self) -> &str {
33        match self {
34            AccountStatus::Enabled => "enabled",
35            AccountStatus::Disabled => "disabled",
36        }
37    }
38}
39
40impl TryFrom<&str> for AccountStatus {
41    type Error = String;
42
43    fn try_from(s: &str) -> Result<Self, Self::Error> {
44        match s {
45            "enabled" => Ok(AccountStatus::Enabled),
46            "disabled" => Ok(AccountStatus::Disabled),
47            _ => Err(format!("invalid account status: {s}")),
48        }
49    }
50}
51
52#[derive(Debug, Serialize, Deserialize)]
53pub enum UserAuthType {
54    #[serde(rename = "builtin")]
55    Builtin,
56    #[serde(rename = "ldap")]
57    Ldap,
58}
59
60#[derive(Debug, Serialize, Deserialize)]
61pub struct UserAuthInfo {
62    #[serde(rename = "type")]
63    pub auth_type: UserAuthType,
64
65    #[serde(rename = "authServer", skip_serializing_if = "Option::is_none")]
66    pub auth_server: Option<String>,
67
68    #[serde(rename = "authServerUserID", skip_serializing_if = "Option::is_none")]
69    pub auth_server_user_id: Option<String>,
70}
71
72#[derive(Debug, Serialize, Deserialize, Default)]
73pub struct UserInfo {
74    #[serde(rename = "userAuthInfo", skip_serializing_if = "Option::is_none")]
75    pub auth_info: Option<UserAuthInfo>,
76
77    #[serde(rename = "secretKey", skip_serializing_if = "Option::is_none")]
78    pub secret_key: Option<String>,
79
80    #[serde(rename = "policyName", skip_serializing_if = "Option::is_none")]
81    pub policy_name: Option<String>,
82
83    #[serde(rename = "status")]
84    pub status: AccountStatus,
85
86    #[serde(rename = "memberOf", skip_serializing_if = "Option::is_none")]
87    pub member_of: Option<Vec<String>>,
88
89    #[serde(rename = "updatedAt")]
90    pub updated_at: Option<OffsetDateTime>,
91}
92
93#[derive(Debug, Serialize, Deserialize)]
94pub struct AddOrUpdateUserReq {
95    #[serde(rename = "secretKey")]
96    pub secret_key: String,
97
98    #[serde(rename = "policy", skip_serializing_if = "Option::is_none")]
99    pub policy: Option<String>,
100
101    #[serde(rename = "status")]
102    pub status: AccountStatus,
103}
104
105#[derive(Debug, Serialize, Deserialize)]
106pub struct ServiceAccountInfo {
107    #[serde(rename = "parentUser")]
108    pub parent_user: String,
109
110    #[serde(rename = "accountStatus")]
111    pub account_status: String,
112
113    #[serde(rename = "impliedPolicy")]
114    pub implied_policy: bool,
115
116    #[serde(rename = "accessKey")]
117    pub access_key: String,
118
119    #[serde(rename = "name", skip_serializing_if = "Option::is_none")]
120    pub name: Option<String>,
121
122    #[serde(rename = "description", skip_serializing_if = "Option::is_none")]
123    pub description: Option<String>,
124
125    #[serde(rename = "expiration", with = "time::serde::rfc3339::option")]
126    pub expiration: Option<OffsetDateTime>,
127}
128
129#[derive(Debug, Serialize, Deserialize)]
130pub struct ListServiceAccountsResp {
131    #[serde(rename = "accounts")]
132    pub accounts: Vec<ServiceAccountInfo>,
133}
134
135#[derive(Debug, Serialize, Deserialize)]
136pub struct AddServiceAccountReq {
137    #[serde(rename = "policy", skip_serializing_if = "Option::is_none")]
138    pub policy: Option<String>,
139
140    #[serde(rename = "targetUser", skip_serializing_if = "Option::is_none")]
141    pub target_user: Option<String>,
142
143    #[serde(rename = "accessKey")]
144    pub access_key: String,
145
146    #[serde(rename = "secretKey")]
147    pub secret_key: String,
148
149    #[serde(rename = "name")]
150    pub name: Option<String>,
151
152    #[serde(rename = "description", skip_serializing_if = "Option::is_none")]
153    pub description: Option<String>,
154
155    #[serde(rename = "expiration", with = "time::serde::rfc3339::option")]
156    pub expiration: Option<OffsetDateTime>,
157}
158
159impl AddServiceAccountReq {
160    pub fn validate(&self) -> Result<(), String> {
161        if self.access_key.is_empty() {
162            return Err("accessKey is empty".to_string());
163        }
164
165        if self.secret_key.is_empty() {
166            return Err("secretKey is empty".to_string());
167        }
168
169        if self.name.is_none() {
170            return Err("name is empty".to_string());
171        }
172
173        // TODO: validate
174
175        Ok(())
176    }
177}
178
179#[derive(Serialize)]
180#[serde(rename_all = "camelCase")]
181pub struct Credentials<'a> {
182    pub access_key: &'a str,
183    pub secret_key: &'a str,
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub session_token: Option<&'a str>,
186    #[serde(skip_serializing_if = "Option::is_none")]
187    #[serde(with = "time::serde::rfc3339::option")]
188    pub expiration: Option<OffsetDateTime>,
189}
190
191#[derive(Serialize)]
192pub struct AddServiceAccountResp<'a> {
193    pub credentials: Credentials<'a>,
194}
195
196#[derive(Serialize)]
197#[serde(rename_all = "camelCase")]
198pub struct InfoServiceAccountResp {
199    pub parent_user: String,
200    pub account_status: String,
201    pub implied_policy: bool,
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub policy: Option<String>,
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub name: Option<String>,
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub description: Option<String>,
208
209    #[serde(skip_serializing_if = "Option::is_none")]
210    #[serde(with = "time::serde::rfc3339::option")]
211    pub expiration: Option<OffsetDateTime>,
212}
213
214#[derive(Debug, Serialize, Deserialize)]
215pub struct UpdateServiceAccountReq {
216    #[serde(rename = "newPolicy", skip_serializing_if = "Option::is_none")]
217    pub new_policy: Option<String>,
218
219    #[serde(rename = "newSecretKey", skip_serializing_if = "Option::is_none")]
220    pub new_secret_key: Option<String>,
221
222    #[serde(rename = "newStatus", skip_serializing_if = "Option::is_none")]
223    pub new_status: Option<String>,
224
225    #[serde(rename = "newName", skip_serializing_if = "Option::is_none")]
226    pub new_name: Option<String>,
227
228    #[serde(rename = "newDescription", skip_serializing_if = "Option::is_none")]
229    pub new_description: Option<String>,
230
231    #[serde(rename = "newExpiration", skip_serializing_if = "Option::is_none")]
232    #[serde(with = "time::serde::rfc3339::option")]
233    pub new_expiration: Option<OffsetDateTime>,
234}
235
236impl UpdateServiceAccountReq {
237    pub fn validate(&self) -> Result<(), String> {
238        // TODO: validate
239        Ok(())
240    }
241}
242
243#[derive(Debug, Serialize, Deserialize, Default)]
244pub struct AccountInfo {
245    pub account_name: String,
246    pub server: BackendInfo,
247    pub policy: serde_json::Value, // Use iam/policy::parse to parse the result, to be done by the caller.
248    pub buckets: Vec<BucketAccessInfo>,
249}
250
251#[derive(Debug, Serialize, Deserialize, Default)]
252pub struct BucketAccessInfo {
253    pub name: String,
254    pub size: u64,
255    pub objects: u64,
256    pub object_sizes_histogram: HashMap<String, u64>,
257    pub object_versions_histogram: HashMap<String, u64>,
258    pub details: Option<BucketDetails>,
259    pub prefix_usage: HashMap<String, u64>,
260    #[serde(rename = "expiration", with = "time::serde::rfc3339::option")]
261    pub created: Option<OffsetDateTime>,
262    pub access: AccountAccess,
263}
264
265#[derive(Debug, Serialize, Deserialize, Default)]
266pub struct BucketDetails {
267    pub versioning: bool,
268    pub versioning_suspended: bool,
269    pub locking: bool,
270    pub replication: bool,
271    // pub tagging: Option<Tagging>,
272}
273
274#[derive(Debug, Serialize, Deserialize, Default)]
275pub struct AccountAccess {
276    pub read: bool,
277    pub write: bool,
278}
279
280/// SRSessionPolicy - represents a session policy to be replicated.
281#[derive(Debug, Clone)]
282pub struct SRSessionPolicy(Option<Box<RawValue>>);
283
284impl SRSessionPolicy {
285    pub fn new() -> Self {
286        SRSessionPolicy(None)
287    }
288
289    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
290        if json == "null" {
291            Ok(SRSessionPolicy(None))
292        } else {
293            let raw_value = serde_json::from_str(json)?;
294            Ok(SRSessionPolicy(Some(raw_value)))
295        }
296    }
297
298    pub fn is_null(&self) -> bool {
299        self.0.is_none()
300    }
301
302    pub fn as_str(&self) -> Option<&str> {
303        self.0.as_ref().map(|v| v.get())
304    }
305}
306
307impl Default for SRSessionPolicy {
308    fn default() -> Self {
309        Self::new()
310    }
311}
312
313impl PartialEq for SRSessionPolicy {
314    fn eq(&self, other: &Self) -> bool {
315        self.0.as_ref().map(|v| v.get()) == other.0.as_ref().map(|v| v.get())
316    }
317}
318
319impl Serialize for SRSessionPolicy {
320    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
321    where
322        S: Serializer,
323    {
324        match &self.0 {
325            Some(raw_value) => raw_value.serialize(serializer),
326            None => serializer.serialize_none(),
327        }
328    }
329}
330
331impl<'de> Deserialize<'de> for SRSessionPolicy {
332    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
333    where
334        D: Deserializer<'de>,
335    {
336        let raw_value: Option<Box<RawValue>> = Option::deserialize(deserializer)?;
337        Ok(SRSessionPolicy(raw_value))
338    }
339}
340
341/// SRSvcAccCreate - create operation
342#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct SRSvcAccCreate {
344    pub parent: String,
345
346    #[serde(rename = "accessKey")]
347    pub access_key: String,
348
349    #[serde(rename = "secretKey")]
350    pub secret_key: String,
351
352    pub groups: Vec<String>,
353
354    pub claims: HashMap<String, serde_json::Value>,
355
356    #[serde(rename = "sessionPolicy")]
357    pub session_policy: SRSessionPolicy,
358
359    pub status: String,
360
361    pub name: String,
362
363    pub description: String,
364
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub expiration: Option<OffsetDateTime>,
367
368    #[serde(rename = "apiVersion", skip_serializing_if = "Option::is_none")]
369    pub api_version: Option<String>,
370}
371
372/// ImportIAMResult - represents the structure iam import response
373#[derive(Debug, Clone, Serialize, Deserialize, Default)]
374pub struct ImportIAMResult {
375    /// Skipped entries while import
376    /// This could be due to groups, policies etc missing for
377    /// imported entries. We dont fail hard in this case and
378    pub skipped: IAMEntities,
379
380    /// Removed entries - this mostly happens for policies
381    /// where empty might be getting imported and that's invalid
382    pub removed: IAMEntities,
383
384    /// Newly added entries
385    pub added: IAMEntities,
386
387    /// Failed entries while import. This would have details of
388    /// failed entities with respective errors
389    pub failed: IAMErrEntities,
390}
391
392/// IAMEntities - represents different IAM entities
393#[derive(Default, Debug, Clone, Serialize, Deserialize)]
394pub struct IAMEntities {
395    /// List of policy names
396    pub policies: Vec<String>,
397
398    /// List of user names
399    pub users: Vec<String>,
400
401    /// List of group names
402    pub groups: Vec<String>,
403
404    /// List of Service Account names
405    #[serde(rename = "serviceAccounts")]
406    pub service_accounts: Vec<String>,
407
408    /// List of user policies, each entry in map represents list of policies
409    /// applicable to the user
410    #[serde(rename = "userPolicies")]
411    pub user_policies: Vec<HashMap<String, Vec<String>>>,
412
413    /// List of group policies, each entry in map represents list of policies
414    /// applicable to the group
415    #[serde(rename = "groupPolicies")]
416    pub group_policies: Vec<HashMap<String, Vec<String>>>,
417
418    /// List of STS policies, each entry in map represents list of policies
419    /// applicable to the STS
420    #[serde(rename = "stsPolicies")]
421    pub sts_policies: Vec<HashMap<String, Vec<String>>>,
422}
423
424/// IAMErrEntities - represents errored out IAM entries while import with error
425#[derive(Debug, Clone, Serialize, Deserialize, Default)]
426pub struct IAMErrEntities {
427    /// List of errored out policies with errors
428    pub policies: Vec<IAMErrEntity>,
429
430    /// List of errored out users with errors
431    pub users: Vec<IAMErrEntity>,
432
433    /// List of errored out groups with errors
434    pub groups: Vec<IAMErrEntity>,
435
436    /// List of errored out service accounts with errors
437    #[serde(rename = "serviceAccounts")]
438    pub service_accounts: Vec<IAMErrEntity>,
439
440    /// List of errored out user policies with errors
441    #[serde(rename = "userPolicies")]
442    pub user_policies: Vec<IAMErrPolicyEntity>,
443
444    /// List of errored out group policies with errors
445    #[serde(rename = "groupPolicies")]
446    pub group_policies: Vec<IAMErrPolicyEntity>,
447
448    /// List of errored out STS policies with errors
449    #[serde(rename = "stsPolicies")]
450    pub sts_policies: Vec<IAMErrPolicyEntity>,
451}
452
453/// IAMErrEntity - represents an errored IAM entity with error details
454#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct IAMErrEntity {
456    pub name: String,
457    pub error: String,
458}
459
460/// IAMErrPolicyEntity - represents an errored policy entity with error details
461#[derive(Debug, Clone, Serialize, Deserialize)]
462pub struct IAMErrPolicyEntity {
463    pub name: String,
464    pub policies: Vec<String>,
465    pub error: String,
466}
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471    use serde_json;
472    use time::OffsetDateTime;
473
474    #[test]
475    fn test_account_status_default() {
476        let status = AccountStatus::default();
477        assert_eq!(status, AccountStatus::Disabled);
478    }
479
480    #[test]
481    fn test_account_status_as_ref() {
482        assert_eq!(AccountStatus::Enabled.as_ref(), "enabled");
483        assert_eq!(AccountStatus::Disabled.as_ref(), "disabled");
484    }
485
486    #[test]
487    fn test_account_status_try_from_valid() {
488        assert_eq!(AccountStatus::try_from("enabled").unwrap(), AccountStatus::Enabled);
489        assert_eq!(AccountStatus::try_from("disabled").unwrap(), AccountStatus::Disabled);
490    }
491
492    #[test]
493    fn test_account_status_try_from_invalid() {
494        let result = AccountStatus::try_from("invalid");
495        assert!(result.is_err());
496        assert!(result.unwrap_err().contains("invalid account status"));
497    }
498
499    #[test]
500    fn test_account_status_serialization() {
501        let enabled = AccountStatus::Enabled;
502        let disabled = AccountStatus::Disabled;
503
504        let enabled_json = serde_json::to_string(&enabled).unwrap();
505        let disabled_json = serde_json::to_string(&disabled).unwrap();
506
507        assert_eq!(enabled_json, "\"enabled\"");
508        assert_eq!(disabled_json, "\"disabled\"");
509    }
510
511    #[test]
512    fn test_account_status_deserialization() {
513        let enabled: AccountStatus = serde_json::from_str("\"enabled\"").unwrap();
514        let disabled: AccountStatus = serde_json::from_str("\"disabled\"").unwrap();
515
516        assert_eq!(enabled, AccountStatus::Enabled);
517        assert_eq!(disabled, AccountStatus::Disabled);
518    }
519
520    #[test]
521    fn test_user_auth_type_serialization() {
522        let builtin = UserAuthType::Builtin;
523        let ldap = UserAuthType::Ldap;
524
525        let builtin_json = serde_json::to_string(&builtin).unwrap();
526        let ldap_json = serde_json::to_string(&ldap).unwrap();
527
528        assert_eq!(builtin_json, "\"builtin\"");
529        assert_eq!(ldap_json, "\"ldap\"");
530    }
531
532    #[test]
533    fn test_user_auth_info_creation() {
534        let auth_info = UserAuthInfo {
535            auth_type: UserAuthType::Ldap,
536            auth_server: Some("ldap.example.com".to_string()),
537            auth_server_user_id: Some("user123".to_string()),
538        };
539
540        assert!(matches!(auth_info.auth_type, UserAuthType::Ldap));
541        assert_eq!(auth_info.auth_server.unwrap(), "ldap.example.com");
542        assert_eq!(auth_info.auth_server_user_id.unwrap(), "user123");
543    }
544
545    #[test]
546    fn test_user_auth_info_serialization() {
547        let auth_info = UserAuthInfo {
548            auth_type: UserAuthType::Builtin,
549            auth_server: None,
550            auth_server_user_id: None,
551        };
552
553        let json = serde_json::to_string(&auth_info).unwrap();
554        assert!(json.contains("builtin"));
555        assert!(!json.contains("authServer"), "None fields should be skipped");
556    }
557
558    #[test]
559    fn test_user_info_default() {
560        let user_info = UserInfo::default();
561        assert!(user_info.auth_info.is_none());
562        assert!(user_info.secret_key.is_none());
563        assert!(user_info.policy_name.is_none());
564        assert_eq!(user_info.status, AccountStatus::Disabled);
565        assert!(user_info.member_of.is_none());
566        assert!(user_info.updated_at.is_none());
567    }
568
569    #[test]
570    fn test_user_info_with_values() {
571        let now = OffsetDateTime::now_utc();
572        let user_info = UserInfo {
573            auth_info: Some(UserAuthInfo {
574                auth_type: UserAuthType::Builtin,
575                auth_server: None,
576                auth_server_user_id: None,
577            }),
578            secret_key: Some("secret123".to_string()),
579            policy_name: Some("ReadOnlyAccess".to_string()),
580            status: AccountStatus::Enabled,
581            member_of: Some(vec!["group1".to_string(), "group2".to_string()]),
582            updated_at: Some(now),
583        };
584
585        assert!(user_info.auth_info.is_some());
586        assert_eq!(user_info.secret_key.unwrap(), "secret123");
587        assert_eq!(user_info.policy_name.unwrap(), "ReadOnlyAccess");
588        assert_eq!(user_info.status, AccountStatus::Enabled);
589        assert_eq!(user_info.member_of.unwrap().len(), 2);
590        assert!(user_info.updated_at.is_some());
591    }
592
593    #[test]
594    fn test_add_or_update_user_req_creation() {
595        let req = AddOrUpdateUserReq {
596            secret_key: "newsecret".to_string(),
597            policy: Some("FullAccess".to_string()),
598            status: AccountStatus::Enabled,
599        };
600
601        assert_eq!(req.secret_key, "newsecret");
602        assert_eq!(req.policy.unwrap(), "FullAccess");
603        assert_eq!(req.status, AccountStatus::Enabled);
604    }
605
606    #[test]
607    fn test_service_account_info_creation() {
608        let now = OffsetDateTime::now_utc();
609        let service_account = ServiceAccountInfo {
610            parent_user: "admin".to_string(),
611            account_status: "enabled".to_string(),
612            implied_policy: true,
613            access_key: "AKIAIOSFODNN7EXAMPLE".to_string(),
614            name: Some("test-service".to_string()),
615            description: Some("Test service account".to_string()),
616            expiration: Some(now),
617        };
618
619        assert_eq!(service_account.parent_user, "admin");
620        assert_eq!(service_account.account_status, "enabled");
621        assert!(service_account.implied_policy);
622        assert_eq!(service_account.access_key, "AKIAIOSFODNN7EXAMPLE");
623        assert_eq!(service_account.name.unwrap(), "test-service");
624        assert!(service_account.expiration.is_some());
625    }
626
627    #[test]
628    fn test_list_service_accounts_resp_creation() {
629        let resp = ListServiceAccountsResp {
630            accounts: vec![
631                ServiceAccountInfo {
632                    parent_user: "user1".to_string(),
633                    account_status: "enabled".to_string(),
634                    implied_policy: false,
635                    access_key: "KEY1".to_string(),
636                    name: Some("service1".to_string()),
637                    description: None,
638                    expiration: None,
639                },
640                ServiceAccountInfo {
641                    parent_user: "user2".to_string(),
642                    account_status: "disabled".to_string(),
643                    implied_policy: true,
644                    access_key: "KEY2".to_string(),
645                    name: Some("service2".to_string()),
646                    description: Some("Second service".to_string()),
647                    expiration: None,
648                },
649            ],
650        };
651
652        assert_eq!(resp.accounts.len(), 2);
653        assert_eq!(resp.accounts[0].parent_user, "user1");
654        assert_eq!(resp.accounts[1].account_status, "disabled");
655    }
656
657    #[test]
658    fn test_add_service_account_req_validate_success() {
659        let req = AddServiceAccountReq {
660            policy: Some("ReadOnlyAccess".to_string()),
661            target_user: Some("testuser".to_string()),
662            access_key: "AKIAIOSFODNN7EXAMPLE".to_string(),
663            secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string(),
664            name: Some("test-service".to_string()),
665            description: Some("Test service account".to_string()),
666            expiration: None,
667        };
668
669        let result = req.validate();
670        assert!(result.is_ok());
671    }
672
673    #[test]
674    fn test_add_service_account_req_validate_empty_access_key() {
675        let req = AddServiceAccountReq {
676            policy: None,
677            target_user: None,
678            access_key: "".to_string(),
679            secret_key: "secret".to_string(),
680            name: Some("test".to_string()),
681            description: None,
682            expiration: None,
683        };
684
685        let result = req.validate();
686        assert!(result.is_err());
687        assert!(result.unwrap_err().contains("accessKey is empty"));
688    }
689
690    #[test]
691    fn test_add_service_account_req_validate_empty_secret_key() {
692        let req = AddServiceAccountReq {
693            policy: None,
694            target_user: None,
695            access_key: "AKIAIOSFODNN7EXAMPLE".to_string(),
696            secret_key: "".to_string(),
697            name: Some("test".to_string()),
698            description: None,
699            expiration: None,
700        };
701
702        let result = req.validate();
703        assert!(result.is_err());
704        assert!(result.unwrap_err().contains("secretKey is empty"));
705    }
706
707    #[test]
708    fn test_add_service_account_req_validate_empty_name() {
709        let req = AddServiceAccountReq {
710            policy: None,
711            target_user: None,
712            access_key: "AKIAIOSFODNN7EXAMPLE".to_string(),
713            secret_key: "secret".to_string(),
714            name: None,
715            description: None,
716            expiration: None,
717        };
718
719        let result = req.validate();
720        assert!(result.is_err());
721        assert!(result.unwrap_err().contains("name is empty"));
722    }
723
724    #[test]
725    fn test_credentials_serialization() {
726        let now = OffsetDateTime::now_utc();
727        let credentials = Credentials {
728            access_key: "AKIAIOSFODNN7EXAMPLE",
729            secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
730            session_token: Some("session123"),
731            expiration: Some(now),
732        };
733
734        let json = serde_json::to_string(&credentials).unwrap();
735        assert!(json.contains("AKIAIOSFODNN7EXAMPLE"));
736        assert!(json.contains("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"));
737        assert!(json.contains("session123"));
738    }
739
740    #[test]
741    fn test_credentials_without_optional_fields() {
742        let credentials = Credentials {
743            access_key: "AKIAIOSFODNN7EXAMPLE",
744            secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
745            session_token: None,
746            expiration: None,
747        };
748
749        let json = serde_json::to_string(&credentials).unwrap();
750        assert!(json.contains("AKIAIOSFODNN7EXAMPLE"));
751        assert!(!json.contains("sessionToken"), "None fields should be skipped");
752        assert!(!json.contains("expiration"), "None fields should be skipped");
753    }
754
755    #[test]
756    fn test_add_service_account_resp_creation() {
757        let credentials = Credentials {
758            access_key: "AKIAIOSFODNN7EXAMPLE",
759            secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
760            session_token: None,
761            expiration: None,
762        };
763
764        let resp = AddServiceAccountResp { credentials };
765
766        assert_eq!(resp.credentials.access_key, "AKIAIOSFODNN7EXAMPLE");
767        assert_eq!(resp.credentials.secret_key, "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY");
768    }
769
770    #[test]
771    fn test_info_service_account_resp_creation() {
772        let now = OffsetDateTime::now_utc();
773        let resp = InfoServiceAccountResp {
774            parent_user: "admin".to_string(),
775            account_status: "enabled".to_string(),
776            implied_policy: true,
777            policy: Some("ReadOnlyAccess".to_string()),
778            name: Some("test-service".to_string()),
779            description: Some("Test service account".to_string()),
780            expiration: Some(now),
781        };
782
783        assert_eq!(resp.parent_user, "admin");
784        assert_eq!(resp.account_status, "enabled");
785        assert!(resp.implied_policy);
786        assert_eq!(resp.policy.unwrap(), "ReadOnlyAccess");
787        assert_eq!(resp.name.unwrap(), "test-service");
788        assert!(resp.expiration.is_some());
789    }
790
791    #[test]
792    fn test_update_service_account_req_validate() {
793        let req = UpdateServiceAccountReq {
794            new_policy: Some("FullAccess".to_string()),
795            new_secret_key: Some("newsecret".to_string()),
796            new_status: Some("enabled".to_string()),
797            new_name: Some("updated-service".to_string()),
798            new_description: Some("Updated description".to_string()),
799            new_expiration: None,
800        };
801
802        let result = req.validate();
803        assert!(result.is_ok());
804    }
805
806    #[test]
807    fn test_account_info_creation() {
808        use crate::BackendInfo;
809
810        let account_info = AccountInfo {
811            account_name: "testuser".to_string(),
812            server: BackendInfo::default(),
813            policy: serde_json::json!({"Version": "2012-10-17"}),
814            buckets: vec![],
815        };
816
817        assert_eq!(account_info.account_name, "testuser");
818        assert!(account_info.buckets.is_empty());
819        assert!(account_info.policy.is_object());
820    }
821
822    #[test]
823    fn test_bucket_access_info_creation() {
824        let now = OffsetDateTime::now_utc();
825        let mut sizes_histogram = HashMap::new();
826        sizes_histogram.insert("small".to_string(), 100);
827        sizes_histogram.insert("large".to_string(), 50);
828
829        let mut versions_histogram = HashMap::new();
830        versions_histogram.insert("v1".to_string(), 80);
831        versions_histogram.insert("v2".to_string(), 70);
832
833        let mut prefix_usage = HashMap::new();
834        prefix_usage.insert("logs/".to_string(), 1000000);
835        prefix_usage.insert("data/".to_string(), 5000000);
836
837        let bucket_info = BucketAccessInfo {
838            name: "test-bucket".to_string(),
839            size: 6000000,
840            objects: 150,
841            object_sizes_histogram: sizes_histogram,
842            object_versions_histogram: versions_histogram,
843            details: Some(BucketDetails {
844                versioning: true,
845                versioning_suspended: false,
846                locking: true,
847                replication: false,
848            }),
849            prefix_usage,
850            created: Some(now),
851            access: AccountAccess {
852                read: true,
853                write: false,
854            },
855        };
856
857        assert_eq!(bucket_info.name, "test-bucket");
858        assert_eq!(bucket_info.size, 6000000);
859        assert_eq!(bucket_info.objects, 150);
860        assert_eq!(bucket_info.object_sizes_histogram.len(), 2);
861        assert_eq!(bucket_info.object_versions_histogram.len(), 2);
862        assert!(bucket_info.details.is_some());
863        assert_eq!(bucket_info.prefix_usage.len(), 2);
864        assert!(bucket_info.created.is_some());
865        assert!(bucket_info.access.read);
866        assert!(!bucket_info.access.write);
867    }
868
869    #[test]
870    fn test_bucket_details_creation() {
871        let details = BucketDetails {
872            versioning: true,
873            versioning_suspended: false,
874            locking: true,
875            replication: true,
876        };
877
878        assert!(details.versioning);
879        assert!(!details.versioning_suspended);
880        assert!(details.locking);
881        assert!(details.replication);
882    }
883
884    #[test]
885    fn test_account_access_creation() {
886        let read_only = AccountAccess {
887            read: true,
888            write: false,
889        };
890
891        let full_access = AccountAccess { read: true, write: true };
892
893        let no_access = AccountAccess {
894            read: false,
895            write: false,
896        };
897
898        assert!(read_only.read && !read_only.write);
899        assert!(full_access.read && full_access.write);
900        assert!(!no_access.read && !no_access.write);
901    }
902
903    #[test]
904    fn test_serialization_deserialization_roundtrip() {
905        let user_info = UserInfo {
906            auth_info: Some(UserAuthInfo {
907                auth_type: UserAuthType::Ldap,
908                auth_server: Some("ldap.example.com".to_string()),
909                auth_server_user_id: Some("user123".to_string()),
910            }),
911            secret_key: Some("secret123".to_string()),
912            policy_name: Some("ReadOnlyAccess".to_string()),
913            status: AccountStatus::Enabled,
914            member_of: Some(vec!["group1".to_string()]),
915            updated_at: None,
916        };
917
918        let json = serde_json::to_string(&user_info).unwrap();
919        let deserialized: UserInfo = serde_json::from_str(&json).unwrap();
920
921        assert_eq!(deserialized.secret_key.unwrap(), "secret123");
922        assert_eq!(deserialized.policy_name.unwrap(), "ReadOnlyAccess");
923        assert_eq!(deserialized.status, AccountStatus::Enabled);
924        assert_eq!(deserialized.member_of.unwrap().len(), 1);
925    }
926
927    #[test]
928    fn test_debug_format_all_structures() {
929        let account_status = AccountStatus::Enabled;
930        let user_auth_type = UserAuthType::Builtin;
931        let user_info = UserInfo::default();
932        let service_account = ServiceAccountInfo {
933            parent_user: "test".to_string(),
934            account_status: "enabled".to_string(),
935            implied_policy: false,
936            access_key: "key".to_string(),
937            name: None,
938            description: None,
939            expiration: None,
940        };
941
942        // Test that all structures can be formatted with Debug
943        assert!(!format!("{account_status:?}").is_empty());
944        assert!(!format!("{user_auth_type:?}").is_empty());
945        assert!(!format!("{user_info:?}").is_empty());
946        assert!(!format!("{service_account:?}").is_empty());
947    }
948
949    #[test]
950    fn test_memory_efficiency() {
951        // Test that structures don't use excessive memory
952        assert!(std::mem::size_of::<AccountStatus>() < 100);
953        assert!(std::mem::size_of::<UserAuthType>() < 100);
954        assert!(std::mem::size_of::<UserInfo>() < 2000);
955        assert!(std::mem::size_of::<ServiceAccountInfo>() < 2000);
956        assert!(std::mem::size_of::<AccountAccess>() < 100);
957    }
958
959    #[test]
960    fn test_edge_cases() {
961        // Test empty strings and edge cases
962        let req = AddServiceAccountReq {
963            policy: Some("".to_string()),
964            target_user: Some("".to_string()),
965            access_key: "valid_key".to_string(),
966            secret_key: "valid_secret".to_string(),
967            name: Some("valid_name".to_string()),
968            description: Some("".to_string()),
969            expiration: None,
970        };
971
972        // Should still validate successfully with empty optional strings
973        assert!(req.validate().is_ok());
974
975        // Test very long strings
976        let long_string = "a".repeat(1000);
977        let long_req = AddServiceAccountReq {
978            policy: Some(long_string.clone()),
979            target_user: Some(long_string.clone()),
980            access_key: long_string.clone(),
981            secret_key: long_string.clone(),
982            name: Some(long_string.clone()),
983            description: Some(long_string),
984            expiration: None,
985        };
986
987        assert!(long_req.validate().is_ok());
988    }
989}