Skip to main content

aws_lite_rs/api/
iam.rs

1//! AWS Identity and Access Management (IAM) API client.
2//!
3//! Thin wrapper over generated ops. All URL construction and HTTP methods
4//! are in `ops::iam::IamOps`. This layer adds:
5//! - Ergonomic method signatures
6//! - Convenience parameters for common operations
7
8use crate::{
9    AwsHttpClient, Result,
10    ops::iam::IamOps,
11    types::iam::{
12        AttachRolePolicyRequest, CreateServiceLinkedRoleRequest, CreateServiceLinkedRoleResponse,
13        DeleteAccessKeyRequest, DeleteUserPolicyRequest, DetachRolePolicyRequest,
14        DetachUserPolicyRequest, GenerateCredentialReportResponse, GetAccessKeyLastUsedRequest,
15        GetAccessKeyLastUsedResponse, GetAccountPasswordPolicyResponse, GetAccountSummaryResponse,
16        GetCredentialReportResponse, GetLoginProfileRequest, GetLoginProfileResponse,
17        GetPolicyVersionRequest, GetPolicyVersionResponse, GetUserPolicyRequest,
18        GetUserPolicyResponse, ListAccessKeysRequest, ListAccessKeysResponse,
19        ListAttachedGroupPoliciesRequest, ListAttachedGroupPoliciesResponse,
20        ListAttachedUserPoliciesRequest, ListAttachedUserPoliciesResponse,
21        ListEntitiesForPolicyRequest, ListEntitiesForPolicyResponse, ListGroupsForUserRequest,
22        ListGroupsForUserResponse, ListMFADevicesRequest, ListMFADevicesResponse,
23        ListPoliciesRequest, ListPoliciesResponse, ListRolesRequest, ListRolesResponse,
24        ListServerCertificatesRequest, ListServerCertificatesResponse, ListUserPoliciesRequest,
25        ListUserPoliciesResponse, ListUsersRequest, ListUsersResponse,
26        ListVirtualMFADevicesRequest, ListVirtualMFADevicesResponse, Policy,
27        UpdateAccessKeyRequest, UpdateAccountPasswordPolicyRequest, VirtualMFADevice,
28    },
29};
30
31/// Client for the AWS Identity and Access Management API.
32pub struct IamClient<'a> {
33    ops: IamOps<'a>,
34}
35
36impl<'a> IamClient<'a> {
37    /// Create a new IAM API client.
38    pub(crate) fn new(client: &'a AwsHttpClient) -> Self {
39        Self {
40            ops: IamOps::new(client),
41        }
42    }
43
44    /// List all IAM users in the account.
45    pub async fn list_users(&self) -> Result<ListUsersResponse> {
46        let body = ListUsersRequest::default();
47        self.ops.list_users(&body).await
48    }
49
50    /// List all managed policies attached to the specified IAM user.
51    pub async fn list_attached_user_policies(
52        &self,
53        user_name: &str,
54    ) -> Result<ListAttachedUserPoliciesResponse> {
55        let body = ListAttachedUserPoliciesRequest {
56            user_name: user_name.to_string(),
57            ..Default::default()
58        };
59        self.ops.list_attached_user_policies(&body).await
60    }
61
62    /// Remove a managed policy from an IAM user.
63    pub async fn detach_user_policy(&self, user_name: &str, policy_arn: &str) -> Result<()> {
64        let body = DetachUserPolicyRequest {
65            user_name: user_name.to_string(),
66            policy_arn: policy_arn.to_string(),
67        };
68        self.ops.detach_user_policy(&body).await
69    }
70
71    /// Delete an access key pair for an IAM user.
72    pub async fn delete_access_key(&self, user_name: &str, access_key_id: &str) -> Result<()> {
73        let body = DeleteAccessKeyRequest {
74            user_name: Some(user_name.to_string()),
75            access_key_id: access_key_id.to_string(),
76        };
77        self.ops.delete_access_key(&body).await
78    }
79
80    /// List access keys for an IAM user.
81    pub async fn list_access_keys(&self, user_name: &str) -> Result<ListAccessKeysResponse> {
82        let body = ListAccessKeysRequest {
83            user_name: Some(user_name.to_string()),
84            ..Default::default()
85        };
86        self.ops.list_access_keys(&body).await
87    }
88
89    /// Retrieve information about when an access key was last used.
90    pub async fn get_access_key_last_used(
91        &self,
92        access_key_id: &str,
93    ) -> Result<GetAccessKeyLastUsedResponse> {
94        let body = GetAccessKeyLastUsedRequest {
95            access_key_id: access_key_id.to_string(),
96        };
97        self.ops.get_access_key_last_used(&body).await
98    }
99
100    /// Generate a credential report for the AWS account.
101    pub async fn generate_credential_report(&self) -> Result<GenerateCredentialReportResponse> {
102        self.ops.generate_credential_report().await
103    }
104
105    /// Retrieve a credential report for the AWS account.
106    pub async fn get_credential_report(&self) -> Result<GetCredentialReportResponse> {
107        self.ops.get_credential_report().await
108    }
109
110    /// Change the status of an access key from Active to Inactive, or vice versa.
111    pub async fn update_access_key(
112        &self,
113        user_name: &str,
114        access_key_id: &str,
115        status: &str,
116    ) -> Result<()> {
117        let body = UpdateAccessKeyRequest {
118            user_name: Some(user_name.to_string()),
119            access_key_id: access_key_id.to_string(),
120            status: status.to_string(),
121        };
122        self.ops.update_access_key(&body).await
123    }
124
125    /// List MFA devices for an IAM user.
126    pub async fn list_mfa_devices(&self, user_name: &str) -> Result<ListMFADevicesResponse> {
127        let body = ListMFADevicesRequest {
128            user_name: Some(user_name.to_string()),
129            ..Default::default()
130        };
131        self.ops.list_mfa_devices(&body).await
132    }
133
134    /// Get the login profile (console password) for an IAM user.
135    pub async fn get_login_profile(&self, user_name: &str) -> Result<GetLoginProfileResponse> {
136        let body = GetLoginProfileRequest {
137            user_name: Some(user_name.to_string()),
138        };
139        self.ops.get_login_profile(&body).await
140    }
141
142    /// Get the account-level summary of IAM entity usage and quotas.
143    pub async fn get_account_summary(&self) -> Result<GetAccountSummaryResponse> {
144        self.ops.get_account_summary().await
145    }
146
147    /// Get the account password policy.
148    pub async fn get_account_password_policy(&self) -> Result<GetAccountPasswordPolicyResponse> {
149        self.ops.get_account_password_policy().await
150    }
151
152    /// Update the account password policy.
153    pub async fn update_account_password_policy(
154        &self,
155        body: &UpdateAccountPasswordPolicyRequest,
156    ) -> Result<()> {
157        self.ops.update_account_password_policy(body).await
158    }
159
160    /// List all IAM roles in the account.
161    pub async fn list_roles(&self) -> Result<ListRolesResponse> {
162        let body = ListRolesRequest::default();
163        self.ops.list_roles(&body).await
164    }
165
166    /// List inline policy names for an IAM user.
167    pub async fn list_user_policies(&self, user_name: &str) -> Result<ListUserPoliciesResponse> {
168        let body = ListUserPoliciesRequest {
169            user_name: user_name.to_string(),
170            ..Default::default()
171        };
172        self.ops.list_user_policies(&body).await
173    }
174
175    /// List the groups that an IAM user belongs to.
176    pub async fn list_groups_for_user(&self, user_name: &str) -> Result<ListGroupsForUserResponse> {
177        let body = ListGroupsForUserRequest {
178            user_name: user_name.to_string(),
179            ..Default::default()
180        };
181        self.ops.list_groups_for_user(&body).await
182    }
183
184    /// List all server certificates in the account.
185    pub async fn list_server_certificates(&self) -> Result<ListServerCertificatesResponse> {
186        let body = ListServerCertificatesRequest::default();
187        self.ops.list_server_certificates(&body).await
188    }
189
190    /// Delete an inline policy from an IAM user.
191    pub async fn delete_user_policy(&self, user_name: &str, policy_name: &str) -> Result<()> {
192        let body = DeleteUserPolicyRequest {
193            user_name: user_name.to_string(),
194            policy_name: policy_name.to_string(),
195        };
196        self.ops.delete_user_policy(&body).await
197    }
198
199    /// Attach a managed policy to an IAM role.
200    pub async fn attach_role_policy(&self, role_name: &str, policy_arn: &str) -> Result<()> {
201        let body = AttachRolePolicyRequest {
202            role_name: role_name.to_string(),
203            policy_arn: policy_arn.to_string(),
204        };
205        self.ops.attach_role_policy(&body).await
206    }
207
208    /// Detach a managed policy from an IAM role.
209    pub async fn detach_role_policy(&self, role_name: &str, policy_arn: &str) -> Result<()> {
210        let body = DetachRolePolicyRequest {
211            role_name: role_name.to_string(),
212            policy_arn: policy_arn.to_string(),
213        };
214        self.ops.detach_role_policy(&body).await
215    }
216
217    /// Create a service-linked role for an AWS service.
218    pub async fn create_service_linked_role(
219        &self,
220        aws_service_name: &str,
221    ) -> Result<CreateServiceLinkedRoleResponse> {
222        let body = CreateServiceLinkedRoleRequest {
223            aws_service_name: aws_service_name.to_string(),
224            ..Default::default()
225        };
226        self.ops.create_service_linked_role(&body).await
227    }
228
229    /// Retrieve an inline policy document embedded in an IAM user.
230    pub async fn get_user_policy(
231        &self,
232        user_name: &str,
233        policy_name: &str,
234    ) -> Result<GetUserPolicyResponse> {
235        let body = GetUserPolicyRequest {
236            user_name: user_name.to_string(),
237            policy_name: policy_name.to_string(),
238        };
239        self.ops.get_user_policy(&body).await
240    }
241
242    /// List all managed policies attached to an IAM group.
243    pub async fn list_attached_group_policies(
244        &self,
245        group_name: &str,
246    ) -> Result<ListAttachedGroupPoliciesResponse> {
247        let body = ListAttachedGroupPoliciesRequest {
248            group_name: group_name.to_string(),
249            ..Default::default()
250        };
251        self.ops.list_attached_group_policies(&body).await
252    }
253
254    // ── Virtual MFA Devices ────────────────────────────────────────────
255
256    /// Return the first page of virtual MFA devices.
257    ///
258    /// `assignment_status`: optional filter — `"Assigned"`, `"Unassigned"`, or
259    /// `"Any"` (default when `None`).
260    pub async fn list_virtual_mfa_devices(
261        &self,
262        assignment_status: Option<&str>,
263    ) -> Result<ListVirtualMFADevicesResponse> {
264        let body = ListVirtualMFADevicesRequest {
265            assignment_status: assignment_status.map(str::to_string),
266            ..Default::default()
267        };
268        self.ops.list_virtual_mfa_devices(&body).await
269    }
270
271    /// Return all virtual MFA devices in the account, following pagination.
272    ///
273    /// CIS 2.5: the root account should use a hardware MFA device, not a
274    /// virtual one. Any `VirtualMFADevice` whose serial number contains
275    /// `"root-account-mfa-device"` indicates virtual MFA on root.
276    pub async fn list_all_virtual_mfa_devices(&self) -> Result<Vec<VirtualMFADevice>> {
277        let mut all = Vec::new();
278        let mut marker: Option<String> = None;
279        loop {
280            let body = ListVirtualMFADevicesRequest {
281                marker: marker.clone(),
282                ..Default::default()
283            };
284            let resp = self.ops.list_virtual_mfa_devices(&body).await?;
285            all.extend(resp.virtual_mfa_devices);
286            match resp.marker {
287                Some(m) if !m.is_empty() && resp.is_truncated == Some(true) => {
288                    marker = Some(m);
289                }
290                _ => break,
291            }
292        }
293        Ok(all)
294    }
295
296    // ── Managed Policies ───────────────────────────────────────────────
297
298    /// Return the first page of IAM policies.
299    ///
300    /// `scope`: `"Local"` (customer-managed), `"AWS"` (AWS-managed), or
301    /// `"All"` (default when `None`).
302    pub async fn list_policies(
303        &self,
304        scope: Option<&str>,
305        marker: Option<&str>,
306    ) -> Result<ListPoliciesResponse> {
307        let body = ListPoliciesRequest {
308            scope: scope.map(str::to_string),
309            marker: marker.map(str::to_string),
310            ..Default::default()
311        };
312        self.ops.list_policies(&body).await
313    }
314
315    /// Return all IAM policies for the given scope, following pagination.
316    ///
317    /// CIS 2.15: pass `scope = Some("Local")` to enumerate customer-managed
318    /// policies, then call `get_policy_version` on each to inspect for
319    /// unrestricted `"*:*"` statements.
320    pub async fn list_all_policies(&self, scope: Option<&str>) -> Result<Vec<Policy>> {
321        let mut all = Vec::new();
322        let mut marker: Option<String> = None;
323        loop {
324            let body = ListPoliciesRequest {
325                scope: scope.map(str::to_string),
326                marker: marker.clone(),
327                ..Default::default()
328            };
329            let resp = self.ops.list_policies(&body).await?;
330            all.extend(resp.policies);
331            match resp.marker {
332                Some(m) if !m.is_empty() && resp.is_truncated == Some(true) => {
333                    marker = Some(m);
334                }
335                _ => break,
336            }
337        }
338        Ok(all)
339    }
340
341    /// Retrieve a specific version of a managed policy.
342    ///
343    /// The `document` field in the response is URL-encoded JSON. Decode it
344    /// with `urlencoding::decode` before parsing.
345    ///
346    /// CIS 2.15: inspect the document for statements allowing `"*"` actions
347    /// on `"*"` resources to detect policies with full admin access.
348    pub async fn get_policy_version(
349        &self,
350        policy_arn: &str,
351        version_id: &str,
352    ) -> Result<GetPolicyVersionResponse> {
353        let body = GetPolicyVersionRequest {
354            policy_arn: policy_arn.to_string(),
355            version_id: version_id.to_string(),
356        };
357        self.ops.get_policy_version(&body).await
358    }
359
360    // ── Entities For Policy ────────────────────────────────────────────
361
362    /// Return the first page of entities (groups, users, roles) attached to a
363    /// managed policy.
364    pub async fn list_entities_for_policy(
365        &self,
366        policy_arn: &str,
367    ) -> Result<ListEntitiesForPolicyResponse> {
368        let body = ListEntitiesForPolicyRequest {
369            policy_arn: policy_arn.to_string(),
370            ..Default::default()
371        };
372        self.ops.list_entities_for_policy(&body).await
373    }
374
375    /// Return all entities attached to a managed policy, following pagination.
376    ///
377    /// Merges `PolicyGroups`, `PolicyUsers`, and `PolicyRoles` across all
378    /// pages into a single response.
379    ///
380    /// CIS 2.16: verify at least one entity is attached to `AWSSupportAccess`.
381    /// CIS 2.21: verify no entity is attached to `AWSCloudShellFullAccess`
382    /// (or only approved entities).
383    pub async fn list_all_entities_for_policy(
384        &self,
385        policy_arn: &str,
386    ) -> Result<ListEntitiesForPolicyResponse> {
387        let mut groups = Vec::new();
388        let mut users = Vec::new();
389        let mut roles = Vec::new();
390        let mut marker: Option<String> = None;
391        loop {
392            let body = ListEntitiesForPolicyRequest {
393                policy_arn: policy_arn.to_string(),
394                marker: marker.clone(),
395                ..Default::default()
396            };
397            let resp = self.ops.list_entities_for_policy(&body).await?;
398            groups.extend(resp.policy_groups);
399            users.extend(resp.policy_users);
400            roles.extend(resp.policy_roles);
401            match resp.marker {
402                Some(m) if !m.is_empty() && resp.is_truncated == Some(true) => {
403                    marker = Some(m);
404                }
405                _ => break,
406            }
407        }
408        Ok(ListEntitiesForPolicyResponse {
409            policy_groups: groups,
410            policy_users: users,
411            policy_roles: roles,
412            is_truncated: Some(false),
413            marker: None,
414        })
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    use crate::AwsHttpClient;
421    use crate::mock_client::MockClient;
422    use crate::test_support::iam_mock_helpers::IamMockHelpers;
423
424    fn xml_envelope(action: &str, inner: &str) -> Vec<u8> {
425        format!("<{action}Response><{action}Result>{inner}</{action}Result></{action}Response>")
426            .into_bytes()
427    }
428
429    #[tokio::test]
430    async fn list_users_returns_parsed_users() {
431        let mut mock = MockClient::new();
432        mock.expect_list_users().returning_bytes(xml_envelope(
433            "ListUsers",
434            "<Users>\
435               <member>\
436                 <Arn>arn:aws:iam::123456789012:user/alice</Arn>\
437                 <UserName>alice</UserName>\
438                 <CreateDate>2024-01-15T10:30:00Z</CreateDate>\
439               </member>\
440             </Users>",
441        ));
442
443        let client = AwsHttpClient::from_mock(mock);
444        let response = client.iam().list_users().await.unwrap();
445
446        assert_eq!(response.users.len(), 1);
447        assert_eq!(response.users[0].user_name, "alice");
448        assert_eq!(
449            response.users[0].arn,
450            "arn:aws:iam::123456789012:user/alice"
451        );
452        assert_eq!(response.users[0].create_date, "2024-01-15T10:30:00Z");
453    }
454
455    #[tokio::test]
456    async fn list_users_handles_empty_response() {
457        let mut mock = MockClient::new();
458        mock.expect_list_users()
459            .returning_bytes(xml_envelope("ListUsers", "<Users/>"));
460
461        let client = AwsHttpClient::from_mock(mock);
462        let response = client.iam().list_users().await.unwrap();
463        assert!(response.users.is_empty());
464    }
465
466    #[tokio::test]
467    async fn list_attached_user_policies_returns_policies() {
468        let mut mock = MockClient::new();
469        mock.expect_list_attached_user_policies()
470            .returning_bytes(xml_envelope(
471                "ListAttachedUserPolicies",
472                "<AttachedPolicies>\
473                   <member>\
474                     <PolicyArn>arn:aws:iam::aws:policy/ReadOnlyAccess</PolicyArn>\
475                     <PolicyName>ReadOnlyAccess</PolicyName>\
476                   </member>\
477                 </AttachedPolicies>",
478            ));
479
480        let client = AwsHttpClient::from_mock(mock);
481        let response = client
482            .iam()
483            .list_attached_user_policies("alice")
484            .await
485            .unwrap();
486
487        assert_eq!(response.attached_policies.len(), 1);
488        assert_eq!(
489            response.attached_policies[0].policy_arn.as_deref(),
490            Some("arn:aws:iam::aws:policy/ReadOnlyAccess")
491        );
492        assert_eq!(
493            response.attached_policies[0].policy_name.as_deref(),
494            Some("ReadOnlyAccess")
495        );
496    }
497
498    #[tokio::test]
499    async fn detach_user_policy_succeeds() {
500        let mut mock = MockClient::new();
501        mock.expect_detach_user_policy()
502            .returning_bytes(xml_envelope("DetachUserPolicy", ""));
503
504        let client = AwsHttpClient::from_mock(mock);
505        let result = client
506            .iam()
507            .detach_user_policy("alice", "arn:aws:iam::aws:policy/ReadOnlyAccess")
508            .await;
509        assert!(result.is_ok());
510    }
511
512    #[tokio::test]
513    async fn delete_access_key_succeeds() {
514        let mut mock = MockClient::new();
515        mock.expect_delete_access_key()
516            .returning_bytes(xml_envelope("DeleteAccessKey", ""));
517
518        let client = AwsHttpClient::from_mock(mock);
519        let result = client
520            .iam()
521            .delete_access_key("alice", "AKIAIOSFODNN7EXAMPLE")
522            .await;
523        assert!(result.is_ok());
524    }
525
526    #[tokio::test]
527    async fn list_access_keys_returns_keys() {
528        let mut mock = MockClient::new();
529        mock.expect_list_access_keys().returning_bytes(xml_envelope(
530            "ListAccessKeys",
531            "<AccessKeyMetadata>\
532               <member>\
533                 <UserName>alice</UserName>\
534                 <AccessKeyId>AKIAIOSFODNN7EXAMPLE</AccessKeyId>\
535                 <Status>Active</Status>\
536                 <CreateDate>2024-03-01T12:00:00Z</CreateDate>\
537               </member>\
538             </AccessKeyMetadata>",
539        ));
540
541        let client = AwsHttpClient::from_mock(mock);
542        let response = client.iam().list_access_keys("alice").await.unwrap();
543
544        assert_eq!(response.access_key_metadata.len(), 1);
545        assert_eq!(
546            response.access_key_metadata[0].access_key_id.as_deref(),
547            Some("AKIAIOSFODNN7EXAMPLE")
548        );
549        assert_eq!(
550            response.access_key_metadata[0].user_name.as_deref(),
551            Some("alice")
552        );
553    }
554
555    #[tokio::test]
556    async fn get_access_key_last_used_returns_info() {
557        let mut mock = MockClient::new();
558        mock.expect_get_access_key_last_used()
559            .returning_bytes(xml_envelope(
560                "GetAccessKeyLastUsed",
561                "<UserName>alice</UserName>\
562                 <AccessKeyLastUsed>\
563                   <ServiceName>s3</ServiceName>\
564                   <Region>us-east-1</Region>\
565                   <LastUsedDate>2024-06-15T08:00:00Z</LastUsedDate>\
566                 </AccessKeyLastUsed>",
567            ));
568
569        let client = AwsHttpClient::from_mock(mock);
570        let response = client
571            .iam()
572            .get_access_key_last_used("AKIAIOSFODNN7EXAMPLE")
573            .await
574            .unwrap();
575
576        assert_eq!(response.user_name.as_deref(), Some("alice"));
577        let last_used = response.access_key_last_used.unwrap();
578        assert_eq!(last_used.service_name, "s3");
579        assert_eq!(last_used.region, "us-east-1");
580        assert_eq!(
581            last_used.last_used_date.as_deref(),
582            Some("2024-06-15T08:00:00Z")
583        );
584    }
585
586    #[tokio::test]
587    async fn generate_credential_report_returns_state() {
588        let mut mock = MockClient::new();
589        mock.expect_generate_credential_report()
590            .returning_bytes(xml_envelope(
591                "GenerateCredentialReport",
592                "<State>STARTED</State>\
593                 <Description>Starting report generation</Description>",
594            ));
595
596        let client = AwsHttpClient::from_mock(mock);
597        let response = client.iam().generate_credential_report().await.unwrap();
598
599        assert_eq!(
600            response.state,
601            Some(crate::types::iam::ReportStateType::Started)
602        );
603        assert_eq!(
604            response.description.as_deref(),
605            Some("Starting report generation")
606        );
607    }
608
609    #[tokio::test]
610    async fn get_credential_report_returns_content() {
611        let mut mock = MockClient::new();
612        mock.expect_get_credential_report()
613            .returning_bytes(xml_envelope(
614                "GetCredentialReport",
615                "<Content>dXNlcixhcm4K</Content>\
616                 <ReportFormat>text/csv</ReportFormat>\
617                 <GeneratedTime>2024-06-15T10:00:00Z</GeneratedTime>",
618            ));
619
620        let client = AwsHttpClient::from_mock(mock);
621        let response = client.iam().get_credential_report().await.unwrap();
622
623        // Content is base64 decoded automatically by serde
624        assert_eq!(response.content.as_deref(), Some("user,arn\n"));
625        assert_eq!(
626            response.report_format,
627            Some(crate::types::iam::ReportFormatType::TextPercsv)
628        );
629        assert_eq!(
630            response.generated_time.as_deref(),
631            Some("2024-06-15T10:00:00Z")
632        );
633    }
634
635    #[tokio::test]
636    async fn update_access_key_succeeds() {
637        let mut mock = MockClient::new();
638        mock.expect_update_access_key()
639            .returning_bytes(xml_envelope("UpdateAccessKey", ""));
640
641        let client = AwsHttpClient::from_mock(mock);
642        let result = client
643            .iam()
644            .update_access_key("alice", "AKIAIOSFODNN7EXAMPLE", "Inactive")
645            .await;
646        assert!(result.is_ok());
647    }
648
649    #[tokio::test]
650    async fn list_mfa_devices_returns_devices() {
651        let mut mock = MockClient::new();
652        mock.expect_list_mfa_devices().returning_bytes(xml_envelope(
653            "ListMFADevices",
654            "<MFADevices>\
655               <member>\
656                 <UserName>alice</UserName>\
657                 <SerialNumber>arn:aws:iam::123456789012:mfa/alice</SerialNumber>\
658                 <EnableDate>2024-01-15T10:00:00Z</EnableDate>\
659               </member>\
660             </MFADevices>",
661        ));
662
663        let client = AwsHttpClient::from_mock(mock);
664        let response = client.iam().list_mfa_devices("alice").await.unwrap();
665
666        assert_eq!(response.mfa_devices.len(), 1);
667        assert_eq!(response.mfa_devices[0].user_name, "alice");
668        assert_eq!(
669            response.mfa_devices[0].serial_number,
670            "arn:aws:iam::123456789012:mfa/alice"
671        );
672        assert_eq!(response.mfa_devices[0].enable_date, "2024-01-15T10:00:00Z");
673    }
674
675    #[tokio::test]
676    async fn get_login_profile_returns_profile() {
677        let mut mock = MockClient::new();
678        mock.expect_get_login_profile()
679            .returning_bytes(xml_envelope(
680                "GetLoginProfile",
681                "<LoginProfile>\
682                   <UserName>alice</UserName>\
683                   <CreateDate>2024-01-15T10:30:00Z</CreateDate>\
684                   <PasswordResetRequired>false</PasswordResetRequired>\
685                 </LoginProfile>",
686            ));
687
688        let client = AwsHttpClient::from_mock(mock);
689        let response = client.iam().get_login_profile("alice").await.unwrap();
690
691        assert_eq!(response.login_profile.user_name, "alice");
692        assert_eq!(response.login_profile.create_date, "2024-01-15T10:30:00Z");
693        assert_eq!(response.login_profile.password_reset_required, Some(false));
694    }
695
696    #[tokio::test]
697    async fn get_account_summary_returns_map() {
698        let mut mock = MockClient::new();
699        mock.expect_get_account_summary()
700            .returning_bytes(xml_envelope(
701                "GetAccountSummary",
702                "<SummaryMap>\
703                   <entry><key>Users</key><value>5</value></entry>\
704                   <entry><key>Roles</key><value>12</value></entry>\
705                   <entry><key>AccountMFAEnabled</key><value>1</value></entry>\
706                 </SummaryMap>",
707            ));
708
709        let client = AwsHttpClient::from_mock(mock);
710        let response = client.iam().get_account_summary().await.unwrap();
711
712        assert_eq!(response.summary_map.len(), 3);
713        assert_eq!(response.summary_map.get("Users"), Some(&5));
714        assert_eq!(response.summary_map.get("Roles"), Some(&12));
715        assert_eq!(response.summary_map.get("AccountMFAEnabled"), Some(&1));
716    }
717
718    #[tokio::test]
719    async fn get_account_password_policy_returns_policy() {
720        let mut mock = MockClient::new();
721        mock.expect_get_account_password_policy()
722            .returning_bytes(xml_envelope(
723                "GetAccountPasswordPolicy",
724                "<PasswordPolicy>\
725                   <MinimumPasswordLength>14</MinimumPasswordLength>\
726                   <RequireSymbols>true</RequireSymbols>\
727                   <RequireNumbers>true</RequireNumbers>\
728                   <RequireUppercaseCharacters>true</RequireUppercaseCharacters>\
729                   <RequireLowercaseCharacters>true</RequireLowercaseCharacters>\
730                   <MaxPasswordAge>90</MaxPasswordAge>\
731                 </PasswordPolicy>",
732            ));
733
734        let client = AwsHttpClient::from_mock(mock);
735        let response = client.iam().get_account_password_policy().await.unwrap();
736
737        let policy = &response.password_policy;
738        assert_eq!(policy.minimum_password_length, Some(14));
739        assert_eq!(policy.require_symbols, Some(true));
740        assert_eq!(policy.require_numbers, Some(true));
741        assert_eq!(policy.require_uppercase_characters, Some(true));
742        assert_eq!(policy.require_lowercase_characters, Some(true));
743        assert_eq!(policy.max_password_age, Some(90));
744    }
745
746    #[tokio::test]
747    async fn update_account_password_policy_succeeds() {
748        let mut mock = MockClient::new();
749        mock.expect_update_account_password_policy()
750            .returning_bytes(xml_envelope("UpdateAccountPasswordPolicy", ""));
751
752        let client = AwsHttpClient::from_mock(mock);
753        let body = crate::types::iam::UpdateAccountPasswordPolicyRequest {
754            minimum_password_length: Some(14),
755            ..Default::default()
756        };
757        let result = client.iam().update_account_password_policy(&body).await;
758        assert!(result.is_ok());
759    }
760
761    #[tokio::test]
762    async fn list_roles_returns_roles() {
763        let mut mock = MockClient::new();
764        mock.expect_list_roles().returning_bytes(xml_envelope(
765            "ListRoles",
766            "<Roles>\
767               <member>\
768                 <RoleName>admin-role</RoleName>\
769                 <Arn>arn:aws:iam::123456789012:role/admin-role</Arn>\
770                 <CreateDate>2024-01-15T10:30:00Z</CreateDate>\
771                 <Description>Admin role</Description>\
772               </member>\
773             </Roles>",
774        ));
775
776        let client = AwsHttpClient::from_mock(mock);
777        let response = client.iam().list_roles().await.unwrap();
778
779        assert_eq!(response.roles.len(), 1);
780        assert_eq!(response.roles[0].role_name, "admin-role");
781        assert_eq!(
782            response.roles[0].arn,
783            "arn:aws:iam::123456789012:role/admin-role"
784        );
785        assert_eq!(response.roles[0].create_date, "2024-01-15T10:30:00Z");
786        assert_eq!(response.roles[0].description.as_deref(), Some("Admin role"));
787    }
788
789    #[tokio::test]
790    async fn list_user_policies_returns_names() {
791        let mut mock = MockClient::new();
792        mock.expect_list_user_policies()
793            .returning_bytes(xml_envelope(
794                "ListUserPolicies",
795                "<PolicyNames>\
796                   <member>s3-read-policy</member>\
797                   <member>ec2-describe-policy</member>\
798                 </PolicyNames>",
799            ));
800
801        let client = AwsHttpClient::from_mock(mock);
802        let response = client.iam().list_user_policies("alice").await.unwrap();
803
804        assert_eq!(response.policy_names.len(), 2);
805        assert_eq!(response.policy_names[0], "s3-read-policy");
806        assert_eq!(response.policy_names[1], "ec2-describe-policy");
807    }
808
809    #[tokio::test]
810    async fn list_groups_for_user_returns_groups() {
811        let mut mock = MockClient::new();
812        mock.expect_list_groups_for_user()
813            .returning_bytes(xml_envelope(
814                "ListGroupsForUser",
815                "<Groups>\
816                   <member>\
817                     <GroupName>developers</GroupName>\
818                     <Arn>arn:aws:iam::123456789012:group/developers</Arn>\
819                     <CreateDate>2024-02-01T09:00:00Z</CreateDate>\
820                   </member>\
821                 </Groups>",
822            ));
823
824        let client = AwsHttpClient::from_mock(mock);
825        let response = client.iam().list_groups_for_user("alice").await.unwrap();
826
827        assert_eq!(response.groups.len(), 1);
828        assert_eq!(response.groups[0].group_name, "developers");
829        assert_eq!(
830            response.groups[0].arn,
831            "arn:aws:iam::123456789012:group/developers"
832        );
833        assert_eq!(response.groups[0].create_date, "2024-02-01T09:00:00Z");
834    }
835
836    #[tokio::test]
837    async fn list_server_certificates_returns_certs() {
838        let mut mock = MockClient::new();
839        mock.expect_list_server_certificates()
840            .returning_bytes(xml_envelope(
841                "ListServerCertificates",
842                "<ServerCertificateMetadataList>\
843                   <member>\
844                     <ServerCertificateName>my-cert</ServerCertificateName>\
845                     <Arn>arn:aws:iam::123456789012:server-certificate/my-cert</Arn>\
846                     <Expiration>2025-12-31T23:59:59Z</Expiration>\
847                     <UploadDate>2024-01-01T00:00:00Z</UploadDate>\
848                   </member>\
849                 </ServerCertificateMetadataList>",
850            ));
851
852        let client = AwsHttpClient::from_mock(mock);
853        let response = client.iam().list_server_certificates().await.unwrap();
854
855        assert_eq!(response.server_certificate_metadata_list.len(), 1);
856        let cert = &response.server_certificate_metadata_list[0];
857        assert_eq!(cert.server_certificate_name, "my-cert");
858        assert_eq!(
859            cert.arn,
860            "arn:aws:iam::123456789012:server-certificate/my-cert"
861        );
862        assert_eq!(cert.expiration.as_deref(), Some("2025-12-31T23:59:59Z"));
863        assert_eq!(cert.upload_date.as_deref(), Some("2024-01-01T00:00:00Z"));
864    }
865
866    #[tokio::test]
867    async fn delete_user_policy_succeeds() {
868        let mut mock = MockClient::new();
869        mock.expect_delete_user_policy()
870            .returning_bytes(xml_envelope("DeleteUserPolicy", ""));
871
872        let client = AwsHttpClient::from_mock(mock);
873        let result = client.iam().delete_user_policy("alice", "my-policy").await;
874        assert!(result.is_ok());
875    }
876
877    #[tokio::test]
878    async fn attach_role_policy_succeeds() {
879        let mut mock = MockClient::new();
880        mock.expect_attach_role_policy()
881            .returning_bytes(xml_envelope("AttachRolePolicy", ""));
882
883        let client = AwsHttpClient::from_mock(mock);
884        let result = client
885            .iam()
886            .attach_role_policy("my-role", "arn:aws:iam::aws:policy/ReadOnlyAccess")
887            .await;
888        assert!(result.is_ok());
889    }
890
891    #[tokio::test]
892    async fn detach_role_policy_succeeds() {
893        let mut mock = MockClient::new();
894        mock.expect_detach_role_policy()
895            .returning_bytes(xml_envelope("DetachRolePolicy", ""));
896
897        let client = AwsHttpClient::from_mock(mock);
898        let result = client
899            .iam()
900            .detach_role_policy("my-role", "arn:aws:iam::aws:policy/ReadOnlyAccess")
901            .await;
902        assert!(result.is_ok());
903    }
904
905    #[tokio::test]
906    async fn create_service_linked_role_returns_role() {
907        let mut mock = MockClient::new();
908        mock.expect_create_service_linked_role()
909            .returning_bytes(xml_envelope(
910                "CreateServiceLinkedRole",
911                "<Role>\
912                   <RoleName>AWSServiceRoleForElasticBeanstalk</RoleName>\
913                   <Arn>arn:aws:iam::123456789012:role/aws-service-role/elasticbeanstalk.amazonaws.com/AWSServiceRoleForElasticBeanstalk</Arn>\
914                   <CreateDate>2024-06-15T12:00:00Z</CreateDate>\
915                   <Description>Service-linked role for Elastic Beanstalk</Description>\
916                 </Role>",
917            ));
918
919        let client = AwsHttpClient::from_mock(mock);
920        let response = client
921            .iam()
922            .create_service_linked_role("elasticbeanstalk.amazonaws.com")
923            .await
924            .unwrap();
925
926        let role = response.role.unwrap();
927        assert_eq!(role.role_name, "AWSServiceRoleForElasticBeanstalk");
928        assert!(role.arn.contains("aws-service-role"));
929        assert_eq!(role.create_date, "2024-06-15T12:00:00Z");
930        assert_eq!(
931            role.description.as_deref(),
932            Some("Service-linked role for Elastic Beanstalk")
933        );
934    }
935
936    #[tokio::test]
937    async fn get_user_policy_returns_document() {
938        let mut mock = MockClient::new();
939        mock.expect_get_user_policy().returning_bytes(xml_envelope(
940            "GetUserPolicy",
941            "<UserName>alice</UserName>\
942             <PolicyName>s3-read-policy</PolicyName>\
943             <PolicyDocument>%7B%22Version%22%3A%222012-10-17%22%7D</PolicyDocument>",
944        ));
945
946        let client = AwsHttpClient::from_mock(mock);
947        let response = client
948            .iam()
949            .get_user_policy("alice", "s3-read-policy")
950            .await
951            .unwrap();
952
953        assert_eq!(response.user_name, "alice");
954        assert_eq!(response.policy_name, "s3-read-policy");
955        assert_eq!(
956            response.policy_document,
957            "%7B%22Version%22%3A%222012-10-17%22%7D"
958        );
959    }
960
961    #[tokio::test]
962    async fn list_attached_group_policies_returns_policies() {
963        let mut mock = MockClient::new();
964        mock.expect_list_attached_group_policies()
965            .returning_bytes(xml_envelope(
966                "ListAttachedGroupPolicies",
967                "<AttachedPolicies>\
968                   <member>\
969                     <PolicyArn>arn:aws:iam::aws:policy/ReadOnlyAccess</PolicyArn>\
970                     <PolicyName>ReadOnlyAccess</PolicyName>\
971                   </member>\
972                 </AttachedPolicies>",
973            ));
974
975        let client = AwsHttpClient::from_mock(mock);
976        let response = client
977            .iam()
978            .list_attached_group_policies("developers")
979            .await
980            .unwrap();
981
982        assert_eq!(response.attached_policies.len(), 1);
983        assert_eq!(
984            response.attached_policies[0].policy_arn.as_deref(),
985            Some("arn:aws:iam::aws:policy/ReadOnlyAccess")
986        );
987        assert_eq!(
988            response.attached_policies[0].policy_name.as_deref(),
989            Some("ReadOnlyAccess")
990        );
991    }
992
993    #[tokio::test]
994    async fn list_attached_group_policies_handles_empty() {
995        let mut mock = MockClient::new();
996        mock.expect_list_attached_group_policies()
997            .returning_bytes(xml_envelope(
998                "ListAttachedGroupPolicies",
999                "<AttachedPolicies/>",
1000            ));
1001
1002        let client = AwsHttpClient::from_mock(mock);
1003        let response = client
1004            .iam()
1005            .list_attached_group_policies("empty-group")
1006            .await
1007            .unwrap();
1008        assert!(response.attached_policies.is_empty());
1009    }
1010
1011    // ── Virtual MFA Devices ────────────────────────────────────────────
1012
1013    #[tokio::test]
1014    async fn list_virtual_mfa_devices_returns_devices() {
1015        let mut mock = MockClient::new();
1016        mock.expect_list_virtual_mfa_devices()
1017            .returning_bytes(xml_envelope(
1018                "ListVirtualMFADevices",
1019                "<VirtualMFADevices>\
1020                   <member>\
1021                     <SerialNumber>arn:aws:iam::123456789012:mfa/root-account-mfa-device</SerialNumber>\
1022                     <EnableDate>2024-01-01T00:00:00Z</EnableDate>\
1023                   </member>\
1024                 </VirtualMFADevices>",
1025            ));
1026
1027        let client = AwsHttpClient::from_mock(mock);
1028        let response = client.iam().list_virtual_mfa_devices(None).await.unwrap();
1029
1030        assert_eq!(response.virtual_mfa_devices.len(), 1);
1031        assert_eq!(
1032            response.virtual_mfa_devices[0].serial_number,
1033            "arn:aws:iam::123456789012:mfa/root-account-mfa-device"
1034        );
1035        assert_eq!(
1036            response.virtual_mfa_devices[0].enable_date.as_deref(),
1037            Some("2024-01-01T00:00:00Z")
1038        );
1039    }
1040
1041    #[tokio::test]
1042    async fn list_virtual_mfa_devices_handles_empty() {
1043        let mut mock = MockClient::new();
1044        mock.expect_list_virtual_mfa_devices()
1045            .returning_bytes(xml_envelope(
1046                "ListVirtualMFADevices",
1047                "<VirtualMFADevices/>",
1048            ));
1049
1050        let client = AwsHttpClient::from_mock(mock);
1051        let response = client.iam().list_virtual_mfa_devices(None).await.unwrap();
1052        assert!(response.virtual_mfa_devices.is_empty());
1053    }
1054
1055    // ── Managed Policies ───────────────────────────────────────────────
1056
1057    #[tokio::test]
1058    async fn list_policies_returns_local_policies() {
1059        let mut mock = MockClient::new();
1060        mock.expect_list_policies().returning_bytes(xml_envelope(
1061            "ListPolicies",
1062            "<Policies>\
1063               <member>\
1064                 <PolicyName>FullAdminPolicy</PolicyName>\
1065                 <PolicyId>ANPA000000000EXAMPLE</PolicyId>\
1066                 <Arn>arn:aws:iam::123456789012:policy/FullAdminPolicy</Arn>\
1067                 <DefaultVersionId>v1</DefaultVersionId>\
1068                 <IsAttachable>true</IsAttachable>\
1069                 <CreateDate>2024-01-15T10:00:00Z</CreateDate>\
1070                 <UpdateDate>2024-01-15T10:00:00Z</UpdateDate>\
1071               </member>\
1072             </Policies>",
1073        ));
1074
1075        let client = AwsHttpClient::from_mock(mock);
1076        let response = client
1077            .iam()
1078            .list_policies(Some("Local"), None)
1079            .await
1080            .unwrap();
1081
1082        assert_eq!(response.policies.len(), 1);
1083        assert_eq!(
1084            response.policies[0].policy_name.as_deref(),
1085            Some("FullAdminPolicy")
1086        );
1087        assert_eq!(
1088            response.policies[0].arn.as_deref(),
1089            Some("arn:aws:iam::123456789012:policy/FullAdminPolicy")
1090        );
1091        assert_eq!(
1092            response.policies[0].default_version_id.as_deref(),
1093            Some("v1")
1094        );
1095        assert_eq!(response.policies[0].is_attachable, Some(true));
1096    }
1097
1098    #[tokio::test]
1099    async fn list_policies_handles_empty() {
1100        let mut mock = MockClient::new();
1101        mock.expect_list_policies()
1102            .returning_bytes(xml_envelope("ListPolicies", "<Policies/>"));
1103
1104        let client = AwsHttpClient::from_mock(mock);
1105        let response = client.iam().list_policies(None, None).await.unwrap();
1106        assert!(response.policies.is_empty());
1107    }
1108
1109    #[tokio::test]
1110    async fn get_policy_version_returns_document() {
1111        let mut mock = MockClient::new();
1112        mock.expect_get_policy_version().returning_bytes(xml_envelope(
1113            "GetPolicyVersion",
1114            "<PolicyVersion>\
1115               <Document>%7B%22Version%22%3A%222012-10-17%22%2C%22Statement%22%3A%5B%7B%22Effect%22%3A%22Allow%22%2C%22Action%22%3A%22%2A%22%2C%22Resource%22%3A%22%2A%22%7D%5D%7D</Document>\
1116               <VersionId>v1</VersionId>\
1117               <IsDefaultVersion>true</IsDefaultVersion>\
1118               <CreateDate>2024-01-15T10:00:00Z</CreateDate>\
1119             </PolicyVersion>",
1120        ));
1121
1122        let client = AwsHttpClient::from_mock(mock);
1123        let response = client
1124            .iam()
1125            .get_policy_version("arn:aws:iam::123456789012:policy/FullAdminPolicy", "v1")
1126            .await
1127            .unwrap();
1128
1129        let version = response.policy_version.unwrap();
1130        assert_eq!(version.version_id.as_deref(), Some("v1"));
1131        assert_eq!(version.is_default_version, Some(true));
1132        // Document is URL-encoded JSON — just verify it's non-empty
1133        assert!(version.document.as_deref().unwrap_or("").contains("%7B"));
1134    }
1135
1136    // ── Entities For Policy ────────────────────────────────────────────
1137
1138    #[tokio::test]
1139    async fn list_entities_for_policy_returns_all_entity_types() {
1140        let mut mock = MockClient::new();
1141        mock.expect_list_entities_for_policy()
1142            .returning_bytes(xml_envelope(
1143                "ListEntitiesForPolicy",
1144                "<PolicyGroups>\
1145                   <member>\
1146                     <GroupName>SupportTeam</GroupName>\
1147                     <GroupId>AGPA000000000EXAMPLE</GroupId>\
1148                   </member>\
1149                 </PolicyGroups>\
1150                 <PolicyUsers>\
1151                   <member>\
1152                     <UserName>support-user</UserName>\
1153                     <UserId>AIDA000000000EXAMPLE</UserId>\
1154                   </member>\
1155                 </PolicyUsers>\
1156                 <PolicyRoles>\
1157                   <member>\
1158                     <RoleName>SupportRole</RoleName>\
1159                     <RoleId>AROA000000000EXAMPLE</RoleId>\
1160                   </member>\
1161                 </PolicyRoles>",
1162            ));
1163
1164        let client = AwsHttpClient::from_mock(mock);
1165        let response = client
1166            .iam()
1167            .list_entities_for_policy("arn:aws:iam::aws:policy/AWSSupportAccess")
1168            .await
1169            .unwrap();
1170
1171        assert_eq!(response.policy_groups.len(), 1);
1172        assert_eq!(
1173            response.policy_groups[0].group_name.as_deref(),
1174            Some("SupportTeam")
1175        );
1176        assert_eq!(response.policy_users.len(), 1);
1177        assert_eq!(
1178            response.policy_users[0].user_name.as_deref(),
1179            Some("support-user")
1180        );
1181        assert_eq!(response.policy_roles.len(), 1);
1182        assert_eq!(
1183            response.policy_roles[0].role_name.as_deref(),
1184            Some("SupportRole")
1185        );
1186    }
1187
1188    #[tokio::test]
1189    async fn list_entities_for_policy_handles_empty() {
1190        let mut mock = MockClient::new();
1191        mock.expect_list_entities_for_policy()
1192            .returning_bytes(xml_envelope(
1193                "ListEntitiesForPolicy",
1194                "<PolicyGroups/><PolicyUsers/><PolicyRoles/>",
1195            ));
1196
1197        let client = AwsHttpClient::from_mock(mock);
1198        let response = client
1199            .iam()
1200            .list_entities_for_policy("arn:aws:iam::aws:policy/AWSCloudShellFullAccess")
1201            .await
1202            .unwrap();
1203        assert!(response.policy_groups.is_empty());
1204        assert!(response.policy_users.is_empty());
1205        assert!(response.policy_roles.is_empty());
1206    }
1207}