Skip to main content

awsim_iam/
lib.rs

1pub mod authz;
2pub mod error;
3mod ids;
4mod operations;
5pub mod state;
6
7/// Re-export the password-verification helper so the operator-auth
8/// flow in the awsim binary can authenticate IAM users against the
9/// bcrypt hash stored on their LoginProfile without each caller
10/// having to depend on the private `operations` module layout.
11pub use operations::users::verify_password;
12
13use std::sync::Arc;
14
15use async_trait::async_trait;
16use awsim_core::{AccountRegionStore, AwsError, Protocol, RequestContext, ServiceHandler};
17use serde_json::Value;
18use tracing::debug;
19
20use state::{DeletionTask, IamState, IamStateSnapshot};
21
22/// IAM is a global service — we use account-only namespacing.
23/// The region key is always "global" for IAM state lookups.
24pub const IAM_REGION: &str = "global";
25
26/// Username reserved for the account owner. Real AWS treats `root`
27/// as the account-creator identity that exists outside the IAM
28/// principal hierarchy; AWSim materializes it as a regular IAM user
29/// at bootstrap so the existing storage and login flows can be
30/// reused, but applies the same protections.
31pub const ROOT_USERNAME: &str = "root";
32
33/// Refuse any mutation that targets the `root` user unless the
34/// caller is an internal server-side flow (bootstrap, background
35/// task). Real AWS keeps root unreachable from the IAM API: an IAM
36/// admin cannot delete the root user, swap its password, attach a
37/// policy, or rotate its access keys. Apply this guard at the top
38/// of every operation in `operations::users`, `operations::policies`,
39/// `operations::groups`, `operations::mfa`, etc. that takes a
40/// `UserName` parameter and mutates state.
41pub fn deny_if_targets_root(user_name: &str, ctx: &RequestContext) -> Result<(), AwsError> {
42    if ctx.internal_bypass {
43        return Ok(());
44    }
45    if user_name == ROOT_USERNAME {
46        return Err(AwsError::access_denied(
47            "The root account owner cannot be modified via the IAM API. \
48             Use the account-owner workflow instead.",
49        ));
50    }
51    Ok(())
52}
53
54/// IAM operations whose `UserName` input parameter must be protected
55/// from targeting the `root` user. Read-only operations (Get*,
56/// List*) are intentionally absent: querying root metadata is
57/// harmless. Pure create-without-target operations like `CreateUser`
58/// are also absent because the bootstrap flow needs to be able to
59/// provision the root record; the check applies to operations that
60/// modify state belonging to an *existing* user.
61const USER_MUTATIONS_REQUIRING_ROOT_PROTECTION: &[&str] = &[
62    "CreateUser",
63    "DeleteUser",
64    "UpdateUser",
65    "TagUser",
66    "UntagUser",
67    "PutUserPermissionsBoundary",
68    "DeleteUserPermissionsBoundary",
69    "CreateLoginProfile",
70    "UpdateLoginProfile",
71    "DeleteLoginProfile",
72    "ChangePassword",
73    "CreateAccessKey",
74    "DeleteAccessKey",
75    "UpdateAccessKey",
76    "AttachUserPolicy",
77    "DetachUserPolicy",
78    "PutUserPolicy",
79    "DeleteUserPolicy",
80    "AddUserToGroup",
81    "RemoveUserFromGroup",
82    "EnableMFADevice",
83    "DeactivateMFADevice",
84    "ResyncMFADevice",
85    "UploadSSHPublicKey",
86    "UpdateSSHPublicKey",
87    "DeleteSSHPublicKey",
88    "UploadSigningCertificate",
89    "UpdateSigningCertificate",
90    "DeleteSigningCertificate",
91    "CreateServiceSpecificCredential",
92    "DeleteServiceSpecificCredential",
93    "UpdateServiceSpecificCredential",
94    "ResetServiceSpecificCredential",
95];
96
97/// The AWSim IAM service handler.
98pub struct IamService {
99    store: AccountRegionStore<IamState>,
100    /// Optional handle to the gateway authz engine. When set, the
101    /// policy simulator pulls in resource policies, SCPs, KMS grants
102    /// — i.e. evaluates the same way the live request path would —
103    /// instead of identity-only.
104    authz: std::sync::OnceLock<Arc<awsim_core::AuthzEngine>>,
105}
106
107impl IamService {
108    pub fn new() -> Self {
109        Self {
110            store: AccountRegionStore::new(),
111            authz: std::sync::OnceLock::new(),
112        }
113    }
114
115    fn get_state(&self, ctx: &RequestContext) -> Arc<IamState> {
116        self.store.get(&ctx.account_id, IAM_REGION)
117    }
118
119    /// Expose the underlying store so the gateway can wire IAM-backed
120    /// principal lookup into the authz engine.
121    pub fn store(&self) -> AccountRegionStore<IamState> {
122        self.store.clone()
123    }
124
125    /// Wire in a handle to the gateway authz engine. Done after the
126    /// engine is fully built (lookups registered) so the simulator
127    /// can use the same trait-object lookups for resource policies,
128    /// SCPs, and grants. Idempotent — first call wins; subsequent
129    /// calls are no-ops.
130    pub fn set_authz(&self, authz: Arc<awsim_core::AuthzEngine>) {
131        let _ = self.authz.set(authz);
132    }
133
134    pub(crate) fn authz(&self) -> Option<&Arc<awsim_core::AuthzEngine>> {
135        self.authz.get()
136    }
137
138    /// Look up the AssumeRolePolicyDocument (trust policy) for `role_arn`
139    /// in `account_id`. Returns None if the role doesn't exist.
140    ///
141    /// Plumbed through to STS so AssumeRole can refuse callers that the
142    /// trust policy doesn't list. Real AWS evaluates the trust policy as
143    /// a resource-based policy with action `sts:AssumeRole` against the
144    /// role ARN, and so does our wiring in awsim-sts.
145    pub fn lookup_role_trust_policy(&self, account_id: &str, role_arn: &str) -> Option<String> {
146        let role_name = role_arn.rsplit_once('/').map(|(_, last)| last)?;
147        let state = self.store.get(account_id, IAM_REGION);
148        state
149            .roles
150            .get(role_name)
151            .map(|r| r.assume_role_policy_document.clone())
152    }
153}
154
155impl Default for IamService {
156    fn default() -> Self {
157        Self::new()
158    }
159}
160
161#[async_trait]
162impl ServiceHandler for IamService {
163    fn service_name(&self) -> &str {
164        "iam"
165    }
166
167    fn signing_name(&self) -> &str {
168        "iam"
169    }
170
171    fn protocol(&self) -> Protocol {
172        Protocol::AwsQuery
173    }
174
175    async fn handle(
176        &self,
177        operation: &str,
178        input: Value,
179        ctx: &RequestContext,
180    ) -> Result<Value, AwsError> {
181        debug!(operation, "IAM request");
182        let state = self.get_state(ctx);
183
184        // Root-user protection precondition. Real AWS forbids any IAM
185        // mutation against the account owner; AWSim mirrors that for
186        // every operation that takes a `UserName` parameter and
187        // changes state. Lookup / list / get operations are not in
188        // this set, since reading root metadata is harmless. The
189        // bootstrap setup endpoint sets `ctx.internal_bypass = true`
190        // so CreateUser("root") + CreateLoginProfile + CreateAccessKey
191        // can run during first-run provisioning.
192        if USER_MUTATIONS_REQUIRING_ROOT_PROTECTION.contains(&operation)
193            && let Some(target) = input.get("UserName").and_then(Value::as_str)
194        {
195            deny_if_targets_root(target, ctx)?;
196        }
197
198        match operation {
199            // Users
200            "CreateUser" => operations::users::create_user(&state, &input, ctx),
201            "GetUser" => operations::users::get_user(&state, &input, ctx),
202            "DeleteUser" => operations::users::delete_user(&state, &input),
203            "ListUsers" => operations::users::list_users(&state, &input),
204            "UpdateUser" => operations::users::update_user(&state, &input),
205
206            // Access Keys
207            "CreateAccessKey" => operations::users::create_access_key(&state, &input, ctx),
208            "DeleteAccessKey" => operations::users::delete_access_key(&state, &input),
209            "ListAccessKeys" => operations::users::list_access_keys(&state, &input, ctx),
210
211            // Groups
212            "CreateGroup" => operations::groups::create_group(&state, &input, ctx),
213            "GetGroup" => operations::groups::get_group(&state, &input),
214            "DeleteGroup" => operations::groups::delete_group(&state, &input),
215            "ListGroups" => operations::groups::list_groups(&state, &input),
216            "AddUserToGroup" => operations::groups::add_user_to_group(&state, &input),
217            "RemoveUserFromGroup" => operations::groups::remove_user_from_group(&state, &input),
218
219            // Roles
220            "CreateRole" => operations::roles::create_role(&state, &input, ctx),
221            "GetRole" => operations::roles::get_role(&state, &input),
222            "DeleteRole" => operations::roles::delete_role(&state, &input),
223            "ListRoles" => operations::roles::list_roles(&state, &input),
224            "UpdateAssumeRolePolicy" => {
225                operations::roles::update_assume_role_policy(&state, &input)
226            }
227            "UpdateRole" => operations::roles::update_role(&state, &input),
228            "UpdateRoleDescription" => operations::roles::update_role_description(&state, &input),
229
230            // Policies (managed)
231            "CreatePolicy" => operations::policies::create_policy(&state, &input, ctx),
232            "GetPolicy" => operations::policies::get_policy(&state, &input),
233            "DeletePolicy" => operations::policies::delete_policy(&state, &input),
234            "ListPolicies" => operations::policies::list_policies(&state, &input),
235
236            // Policy versions
237            "CreatePolicyVersion" => operations::policies::create_policy_version(&state, &input),
238            "DeletePolicyVersion" => operations::policies::delete_policy_version(&state, &input),
239            "GetPolicyVersion" => operations::policies::get_policy_version(&state, &input),
240            "ListPolicyVersions" => operations::policies::list_policy_versions(&state, &input),
241            "SetDefaultPolicyVersion" => {
242                operations::policies::set_default_policy_version(&state, &input)
243            }
244
245            // Attach/detach managed policies
246            "AttachUserPolicy" => operations::policies::attach_user_policy(&state, &input),
247            "DetachUserPolicy" => operations::policies::detach_user_policy(&state, &input),
248            "AttachRolePolicy" => operations::policies::attach_role_policy(&state, &input),
249            "DetachRolePolicy" => operations::policies::detach_role_policy(&state, &input),
250            "AttachGroupPolicy" => operations::policies::attach_group_policy(&state, &input),
251            "DetachGroupPolicy" => operations::policies::detach_group_policy(&state, &input),
252
253            // List attached managed policies
254            "ListAttachedUserPolicies" => {
255                operations::policies::list_attached_user_policies(&state, &input)
256            }
257            "ListAttachedRolePolicies" => {
258                operations::policies::list_attached_role_policies(&state, &input)
259            }
260            "ListAttachedGroupPolicies" => {
261                operations::policies::list_attached_group_policies(&state, &input)
262            }
263
264            // Inline policies — put
265            "PutUserPolicy" => operations::policies::put_user_policy(&state, &input),
266            "PutRolePolicy" => operations::policies::put_role_policy(&state, &input),
267            "PutGroupPolicy" => operations::policies::put_group_policy(&state, &input),
268
269            // Inline policies — user
270            "GetUserPolicy" => operations::users::get_user_policy(&state, &input),
271            "DeleteUserPolicy" => operations::users::delete_user_policy(&state, &input),
272            "ListUserPolicies" => operations::users::list_user_policies(&state, &input),
273
274            // Inline policies — role
275            "GetRolePolicy" => operations::roles::get_role_policy(&state, &input),
276            "DeleteRolePolicy" => operations::roles::delete_role_policy(&state, &input),
277            "ListRolePolicies" => operations::roles::list_role_policies(&state, &input),
278
279            // Inline policies — group
280            "GetGroupPolicy" => operations::groups::get_group_policy(&state, &input),
281            "DeleteGroupPolicy" => operations::groups::delete_group_policy(&state, &input),
282            "ListGroupPolicies" => operations::groups::list_group_policies(&state, &input),
283
284            // Entity queries
285            "ListGroupsForUser" => operations::users::list_groups_for_user(&state, &input),
286            "ListEntitiesForPolicy" => {
287                operations::policies::list_entities_for_policy(&state, &input)
288            }
289
290            // Policy tags
291            "TagPolicy" => operations::policies::tag_policy(&state, &input),
292            "UntagPolicy" => operations::policies::untag_policy(&state, &input),
293            "ListPolicyTags" => operations::policies::list_policy_tags(&state, &input),
294
295            // Instance Profiles
296            "CreateInstanceProfile" => {
297                operations::instance_profiles::create_instance_profile(&state, &input, ctx)
298            }
299            "DeleteInstanceProfile" => {
300                operations::instance_profiles::delete_instance_profile(&state, &input)
301            }
302            "GetInstanceProfile" => {
303                operations::instance_profiles::get_instance_profile(&state, &input)
304            }
305            "ListInstanceProfiles" => {
306                operations::instance_profiles::list_instance_profiles(&state, &input)
307            }
308            "ListInstanceProfilesForRole" => {
309                operations::instance_profiles::list_instance_profiles_for_role(&state, &input)
310            }
311            "AddRoleToInstanceProfile" => {
312                operations::instance_profiles::add_role_to_instance_profile(&state, &input)
313            }
314            "RemoveRoleFromInstanceProfile" => {
315                operations::instance_profiles::remove_role_from_instance_profile(&state, &input)
316            }
317
318            // ── User Tags ─────────────────────────────────────────────────────
319            "TagUser" => operations::tags::tag_user(&state, &input),
320            "UntagUser" => operations::tags::untag_user(&state, &input),
321            "ListUserTags" => operations::tags::list_user_tags(&state, &input),
322
323            // ── Role Tags ─────────────────────────────────────────────────────
324            "TagRole" => operations::tags::tag_role(&state, &input),
325            "UntagRole" => operations::tags::untag_role(&state, &input),
326            "ListRoleTags" => operations::tags::list_role_tags(&state, &input),
327
328            // ── Instance Profile Tags ─────────────────────────────────────────
329            "TagInstanceProfile" => operations::tags::tag_instance_profile(&state, &input),
330            "UntagInstanceProfile" => operations::tags::untag_instance_profile(&state, &input),
331            "ListInstanceProfileTags" => {
332                operations::tags::list_instance_profile_tags(&state, &input)
333            }
334
335            // ── Account Aliases ───────────────────────────────────────────────
336            "CreateAccountAlias" => operations::account::create_account_alias(&state, &input),
337            "DeleteAccountAlias" => operations::account::delete_account_alias(&state, &input),
338            "ListAccountAliases" => operations::account::list_account_aliases(&state, &input),
339
340            // ── Password Policy ───────────────────────────────────────────────
341            "GetAccountPasswordPolicy" => {
342                operations::account::get_account_password_policy(&state, &input)
343            }
344            "UpdateAccountPasswordPolicy" => {
345                operations::account::update_account_password_policy(&state, &input)
346            }
347            "DeleteAccountPasswordPolicy" => {
348                operations::account::delete_account_password_policy(&state, &input)
349            }
350
351            // ── Account Summary / Auth Details ────────────────────────────────
352            "GetAccountSummary" => operations::account::get_account_summary(&state, &input),
353            "GetAccountAuthorizationDetails" => {
354                operations::account::get_account_authorization_details(&state, &input)
355            }
356
357            // ── OIDC Providers ────────────────────────────────────────────────
358            "CreateOpenIDConnectProvider" => {
359                operations::oidc::create_open_id_connect_provider(&state, &input, ctx)
360            }
361            "GetOpenIDConnectProvider" => {
362                operations::oidc::get_open_id_connect_provider(&state, &input)
363            }
364            "ListOpenIDConnectProviders" => {
365                operations::oidc::list_open_id_connect_providers(&state, &input)
366            }
367            "DeleteOpenIDConnectProvider" => {
368                operations::oidc::delete_open_id_connect_provider(&state, &input)
369            }
370            "AddClientIDToOpenIDConnectProvider" => {
371                operations::oidc::add_client_id_to_open_id_connect_provider(&state, &input)
372            }
373            "RemoveClientIDFromOpenIDConnectProvider" => {
374                operations::oidc::remove_client_id_from_open_id_connect_provider(&state, &input)
375            }
376            "UpdateOpenIDConnectProviderThumbprint" => {
377                operations::oidc::update_open_id_connect_provider_thumbprint(&state, &input)
378            }
379
380            // ── SAML Providers ────────────────────────────────────────────────
381            "CreateSAMLProvider" => operations::saml::create_saml_provider(&state, &input, ctx),
382            "GetSAMLProvider" => operations::saml::get_saml_provider(&state, &input),
383            "ListSAMLProviders" => operations::saml::list_saml_providers(&state, &input),
384            "DeleteSAMLProvider" => operations::saml::delete_saml_provider(&state, &input),
385            "UpdateSAMLProvider" => operations::saml::update_saml_provider(&state, &input),
386
387            // ── Server Certificates ───────────────────────────────────────────
388            "UploadServerCertificate" => {
389                operations::certificates::upload_server_certificate(&state, &input, ctx)
390            }
391            "GetServerCertificate" => {
392                operations::certificates::get_server_certificate(&state, &input)
393            }
394            "ListServerCertificates" => {
395                operations::certificates::list_server_certificates(&state, &input)
396            }
397            "DeleteServerCertificate" => {
398                operations::certificates::delete_server_certificate(&state, &input)
399            }
400            "TagServerCertificate" => {
401                operations::certificates::tag_server_certificate(&state, &input)
402            }
403            "UntagServerCertificate" => {
404                operations::certificates::untag_server_certificate(&state, &input)
405            }
406            "ListServerCertificateTags" => {
407                operations::certificates::list_server_certificate_tags(&state, &input)
408            }
409
410            // ── Virtual MFA Devices ───────────────────────────────────────────
411            "CreateVirtualMFADevice" => {
412                operations::mfa::create_virtual_mfa_device(&state, &input, ctx)
413            }
414            "ListVirtualMFADevices" => operations::mfa::list_virtual_mfa_devices(&state, &input),
415            "DeleteVirtualMFADevice" => operations::mfa::delete_virtual_mfa_device(&state, &input),
416            "EnableMFADevice" => operations::mfa::enable_mfa_device(&state, &input),
417            "DeactivateMFADevice" => operations::mfa::deactivate_mfa_device(&state, &input),
418            "ListMFADevices" => operations::mfa::list_mfa_devices(&state, &input),
419
420            // ── SSH Public Keys ───────────────────────────────────────────────
421            "UploadSSHPublicKey" => operations::ssh_keys::upload_ssh_public_key(&state, &input),
422            "GetSSHPublicKey" => operations::ssh_keys::get_ssh_public_key(&state, &input),
423            "ListSSHPublicKeys" => operations::ssh_keys::list_ssh_public_keys(&state, &input),
424            "DeleteSSHPublicKey" => operations::ssh_keys::delete_ssh_public_key(&state, &input),
425            "UpdateSSHPublicKey" => operations::ssh_keys::update_ssh_public_key(&state, &input),
426
427            // ── Login Profiles ────────────────────────────────────────────────
428            "CreateLoginProfile" => operations::users::create_login_profile(&state, &input),
429            "GetLoginProfile" => operations::users::get_login_profile(&state, &input),
430            "UpdateLoginProfile" => operations::users::update_login_profile(&state, &input),
431            "DeleteLoginProfile" => operations::users::delete_login_profile(&state, &input),
432
433            // ── Misc stubs ────────────────────────────────────────────────────
434            "ListServiceSpecificCredentials" => {
435                operations::misc::list_service_specific_credentials(&state, &input)
436            }
437            "ListSigningCertificates" => {
438                operations::misc::list_signing_certificates(&state, &input)
439            }
440            "SimulateCustomPolicy" => {
441                operations::misc::simulate_custom_policy(&state, self.authz(), &input)
442            }
443            "SimulatePrincipalPolicy" => {
444                operations::misc::simulate_principal_policy(&state, self.authz(), &input)
445            }
446            "GetContextKeysForCustomPolicy" => {
447                operations::misc::get_context_keys_for_custom_policy(&state, &input)
448            }
449            "GetContextKeysForPrincipalPolicy" => {
450                operations::misc::get_context_keys_for_principal_policy(&state, &input)
451            }
452
453            // ── Service-Linked Roles ──────────────────────────────────────────
454            "CreateServiceLinkedRole" => {
455                operations::service_linked_roles::create_service_linked_role(&state, &input, ctx)
456            }
457            "DeleteServiceLinkedRole" => {
458                operations::service_linked_roles::delete_service_linked_role(&state, &input)
459            }
460            "GetServiceLinkedRoleDeletionStatus" => {
461                operations::service_linked_roles::get_service_linked_role_deletion_status(
462                    &state, &input,
463                )
464            }
465
466            // ── Credential Report ─────────────────────────────────────────────
467            "GenerateCredentialReport" => {
468                operations::credential_report::generate_credential_report(&state, &input)
469            }
470            "GetCredentialReport" => {
471                operations::credential_report::get_credential_report(&state, &input)
472            }
473
474            // ── Service Last Accessed Details ─────────────────────────────────
475            "GenerateServiceLastAccessedDetails" => {
476                operations::credential_report::generate_service_last_accessed_details(
477                    &state, &input,
478                )
479            }
480            "GetServiceLastAccessedDetails" => {
481                operations::credential_report::get_service_last_accessed_details(&state, &input)
482            }
483            "GetServiceLastAccessedDetailsWithEntities" => {
484                operations::misc::get_service_last_accessed_details_with_entities(&state, &input)
485            }
486
487            // ── Permissions Boundaries ────────────────────────────────────────
488            "PutUserPermissionsBoundary" => {
489                operations::users::put_user_permissions_boundary(&state, &input)
490            }
491            "DeleteUserPermissionsBoundary" => {
492                operations::users::delete_user_permissions_boundary(&state, &input)
493            }
494            "PutRolePermissionsBoundary" => {
495                operations::roles::put_role_permissions_boundary(&state, &input)
496            }
497            "DeleteRolePermissionsBoundary" => {
498                operations::roles::delete_role_permissions_boundary(&state, &input)
499            }
500
501            // ── Access Keys (extended) ────────────────────────────────────────
502            "GetAccessKeyLastUsed" => operations::users::get_access_key_last_used(&state, &input),
503            "UpdateAccessKey" => operations::users::update_access_key(&state, &input),
504            "ChangePassword" => operations::users::change_password(&state, &input),
505
506            // ── Group / Server Certificate updates ────────────────────────────
507            "UpdateGroup" => operations::groups::update_group(&state, &input),
508            "UpdateServerCertificate" => {
509                operations::certificates::update_server_certificate(&state, &input)
510            }
511
512            // ── MFA Device extras ─────────────────────────────────────────────
513            "GetMFADevice" => operations::mfa::get_mfa_device(&state, &input),
514            "ResyncMFADevice" => operations::mfa::resync_mfa_device(&state, &input),
515            "TagMFADevice" => operations::mfa::tag_mfa_device(&state, &input),
516            "UntagMFADevice" => operations::mfa::untag_mfa_device(&state, &input),
517            "ListMFADeviceTags" => operations::mfa::list_mfa_device_tags(&state, &input),
518
519            // ── Signing Certificates ──────────────────────────────────────────
520            "UploadSigningCertificate" => {
521                operations::misc::upload_signing_certificate(&state, &input)
522            }
523            "UpdateSigningCertificate" => {
524                operations::misc::update_signing_certificate(&state, &input)
525            }
526            "DeleteSigningCertificate" => {
527                operations::misc::delete_signing_certificate(&state, &input)
528            }
529
530            // ── Service-Specific Credentials ──────────────────────────────────
531            "CreateServiceSpecificCredential" => {
532                operations::misc::create_service_specific_credential(&state, &input)
533            }
534            "DeleteServiceSpecificCredential" => {
535                operations::misc::delete_service_specific_credential(&state, &input)
536            }
537            "ResetServiceSpecificCredential" => {
538                operations::misc::reset_service_specific_credential(&state, &input)
539            }
540            "UpdateServiceSpecificCredential" => {
541                operations::misc::update_service_specific_credential(&state, &input)
542            }
543
544            // ── Policy lookup helpers ─────────────────────────────────────────
545            "ListPoliciesGrantingServiceAccess" => {
546                operations::misc::list_policies_granting_service_access(&state, &input)
547            }
548            "SetSecurityTokenServicePreferences" => {
549                operations::misc::set_security_token_service_preferences(&state, &input)
550            }
551            "GenerateOrganizationsAccessReport" => {
552                operations::misc::generate_organizations_access_report(&state, &input)
553            }
554            "GetOrganizationsAccessReport" => {
555                operations::misc::get_organizations_access_report(&state, &input)
556            }
557
558            // ── OIDC / SAML provider tags ─────────────────────────────────────
559            "TagOpenIDConnectProvider" => {
560                operations::tags::tag_open_id_connect_provider(&state, &input)
561            }
562            "UntagOpenIDConnectProvider" => {
563                operations::tags::untag_open_id_connect_provider(&state, &input)
564            }
565            "ListOpenIDConnectProviderTags" => {
566                operations::tags::list_open_id_connect_provider_tags(&state, &input)
567            }
568            "TagSAMLProvider" => operations::tags::tag_saml_provider(&state, &input),
569            "UntagSAMLProvider" => operations::tags::untag_saml_provider(&state, &input),
570            "ListSAMLProviderTags" => operations::tags::list_saml_provider_tags(&state, &input),
571
572            _ => Err(AwsError::unknown_operation(operation)),
573        }
574    }
575
576    fn iam_action(&self, operation: &str) -> Option<String> {
577        Some(format!("iam:{operation}"))
578    }
579
580    fn iam_resource(&self, operation: &str, input: &Value, ctx: &RequestContext) -> Option<String> {
581        let prefix = format!("arn:aws:iam::{}", ctx.account_id);
582        match operation {
583            "ListUsers"
584            | "ListGroups"
585            | "ListRoles"
586            | "ListPolicies"
587            | "ListInstanceProfiles"
588            | "ListAccountAliases"
589            | "ListOpenIDConnectProviders"
590            | "ListSAMLProviders"
591            | "ListServerCertificates"
592            | "ListVirtualMFADevices"
593            | "GetAccountSummary"
594            | "GetAccountPasswordPolicy"
595            | "UpdateAccountPasswordPolicy"
596            | "DeleteAccountPasswordPolicy"
597            | "GetAccountAuthorizationDetails"
598            | "GenerateCredentialReport"
599            | "GetCredentialReport"
600            | "GenerateServiceLastAccessedDetails"
601            | "GetServiceLastAccessedDetails"
602            | "GetServiceLastAccessedDetailsWithEntities"
603            | "SimulateCustomPolicy"
604            | "SimulatePrincipalPolicy"
605            | "GetContextKeysForCustomPolicy"
606            | "GetContextKeysForPrincipalPolicy"
607            | "ListServiceSpecificCredentials"
608            | "ListSigningCertificates"
609            | "CreateAccountAlias"
610            | "DeleteAccountAlias" => Some("*".to_string()),
611            op if op.contains("User")
612                && !op.contains("LoginProfile")
613                && !op.contains("AccessKey")
614                && !op.contains("SSHPublicKey")
615                && !op.contains("MFADevice")
616                && !op.contains("ServiceSpecificCredential") =>
617            {
618                input
619                    .get("UserName")
620                    .and_then(|v| v.as_str())
621                    .map(|n| format!("{prefix}:user/{n}"))
622            }
623            "CreateLoginProfile"
624            | "GetLoginProfile"
625            | "UpdateLoginProfile"
626            | "DeleteLoginProfile"
627            | "CreateAccessKey"
628            | "DeleteAccessKey"
629            | "ListAccessKeys"
630            | "UpdateAccessKey"
631            | "GetAccessKeyLastUsed"
632            | "ChangePassword"
633            | "UploadSSHPublicKey"
634            | "GetSSHPublicKey"
635            | "ListSSHPublicKeys"
636            | "DeleteSSHPublicKey"
637            | "UpdateSSHPublicKey"
638            | "EnableMFADevice"
639            | "DeactivateMFADevice"
640            | "ListMFADevices"
641            | "PutUserPermissionsBoundary"
642            | "DeleteUserPermissionsBoundary"
643            | "ListGroupsForUser" => input
644                .get("UserName")
645                .and_then(|v| v.as_str())
646                .map(|n| format!("{prefix}:user/{n}")),
647            op if op.contains("Role")
648                && !op.contains("InstanceProfile")
649                && !op.contains("ServiceLinkedRole") =>
650            {
651                input
652                    .get("RoleName")
653                    .and_then(|v| v.as_str())
654                    .map(|n| format!("{prefix}:role/{n}"))
655            }
656            "PutRolePermissionsBoundary" | "DeleteRolePermissionsBoundary" => input
657                .get("RoleName")
658                .and_then(|v| v.as_str())
659                .map(|n| format!("{prefix}:role/{n}")),
660            "CreateServiceLinkedRole"
661            | "DeleteServiceLinkedRole"
662            | "GetServiceLinkedRoleDeletionStatus" => input
663                .get("RoleName")
664                .and_then(|v| v.as_str())
665                .map(|n| format!("{prefix}:role/aws-service-role/{n}"))
666                .or(Some("*".to_string())),
667            op if op.contains("Group") && !op.contains("ListGroupsForUser") => input
668                .get("GroupName")
669                .and_then(|v| v.as_str())
670                .map(|n| format!("{prefix}:group/{n}")),
671            op if op.contains("InstanceProfile") => input
672                .get("InstanceProfileName")
673                .and_then(|v| v.as_str())
674                .map(|n| format!("{prefix}:instance-profile/{n}")),
675            "CreatePolicy" => input
676                .get("PolicyName")
677                .and_then(|v| v.as_str())
678                .map(|n| format!("{prefix}:policy/{n}")),
679            op if op.contains("Policy") => {
680                if let Some(arn) = input.get("PolicyArn").and_then(|v| v.as_str()) {
681                    Some(arn.to_string())
682                } else if let Some(name) = input.get("PolicyName").and_then(|v| v.as_str()) {
683                    Some(format!("{prefix}:policy/{name}"))
684                } else {
685                    Some("*".to_string())
686                }
687            }
688            op if op.contains("OpenIDConnectProvider") => input
689                .get("OpenIDConnectProviderArn")
690                .and_then(|v| v.as_str())
691                .map(|s| s.to_string())
692                .or_else(|| {
693                    input
694                        .get("Url")
695                        .and_then(|v| v.as_str())
696                        .map(|u| format!("{prefix}:oidc-provider/{u}"))
697                }),
698            op if op.contains("SAMLProvider") => input
699                .get("SAMLProviderArn")
700                .and_then(|v| v.as_str())
701                .map(|s| s.to_string())
702                .or_else(|| {
703                    input
704                        .get("Name")
705                        .and_then(|v| v.as_str())
706                        .map(|n| format!("{prefix}:saml-provider/{n}"))
707                }),
708            op if op.contains("ServerCertificate") => input
709                .get("ServerCertificateName")
710                .and_then(|v| v.as_str())
711                .map(|n| format!("{prefix}:server-certificate/{n}")),
712            op if op.contains("VirtualMFADevice") || op.contains("MFADevice") => input
713                .get("SerialNumber")
714                .and_then(|v| v.as_str())
715                .map(|s| s.to_string())
716                .or_else(|| {
717                    input
718                        .get("VirtualMFADeviceName")
719                        .and_then(|v| v.as_str())
720                        .map(|n| format!("{prefix}:mfa/{n}"))
721                }),
722            _ => Some("*".to_string()),
723        }
724    }
725
726    fn snapshot(&self) -> Option<Vec<u8>> {
727        let mut snapshot = IamStateSnapshot {
728            users: vec![],
729            groups: vec![],
730            roles: vec![],
731            policies: vec![],
732            instance_profiles: vec![],
733            account_aliases: vec![],
734            account_password_policy: None,
735            oidc_providers: vec![],
736            saml_providers: vec![],
737            server_certificates: vec![],
738            virtual_mfa_devices: vec![],
739            login_profiles: vec![],
740            signing_certificates: vec![],
741            service_specific_credentials: vec![],
742            user_permissions_boundaries: vec![],
743            role_permissions_boundaries: vec![],
744            access_key_last_used: vec![],
745            deletion_tasks: vec![],
746        };
747
748        for (_, state) in self.store.iter_all() {
749            snapshot
750                .users
751                .extend(state.users.iter().map(|e| e.value().clone()));
752            snapshot
753                .groups
754                .extend(state.groups.iter().map(|e| e.value().clone()));
755            snapshot
756                .roles
757                .extend(state.roles.iter().map(|e| e.value().clone()));
758            snapshot
759                .policies
760                .extend(state.policies.iter().map(|e| e.value().clone()));
761            snapshot
762                .instance_profiles
763                .extend(state.instance_profiles.iter().map(|e| e.value().clone()));
764            if let Ok(aliases) = state.account_aliases.lock() {
765                snapshot.account_aliases.extend(aliases.clone());
766            }
767            if let Ok(policy) = state.account_password_policy.lock()
768                && snapshot.account_password_policy.is_none()
769            {
770                snapshot.account_password_policy = policy.clone();
771            }
772            snapshot
773                .oidc_providers
774                .extend(state.oidc_providers.iter().map(|e| e.value().clone()));
775            snapshot
776                .saml_providers
777                .extend(state.saml_providers.iter().map(|e| e.value().clone()));
778            snapshot
779                .server_certificates
780                .extend(state.server_certificates.iter().map(|e| e.value().clone()));
781            snapshot
782                .virtual_mfa_devices
783                .extend(state.virtual_mfa_devices.iter().map(|e| e.value().clone()));
784            snapshot
785                .login_profiles
786                .extend(state.login_profiles.iter().map(|e| e.value().clone()));
787            snapshot
788                .signing_certificates
789                .extend(state.signing_certificates.iter().map(|e| e.value().clone()));
790            snapshot.service_specific_credentials.extend(
791                state
792                    .service_specific_credentials
793                    .iter()
794                    .map(|e| e.value().clone()),
795            );
796            snapshot.user_permissions_boundaries.extend(
797                state
798                    .user_permissions_boundaries
799                    .iter()
800                    .map(|e| (e.key().clone(), e.value().clone())),
801            );
802            snapshot.role_permissions_boundaries.extend(
803                state
804                    .role_permissions_boundaries
805                    .iter()
806                    .map(|e| (e.key().clone(), e.value().clone())),
807            );
808            snapshot.access_key_last_used.extend(
809                state
810                    .access_key_last_used
811                    .iter()
812                    .map(|e| (e.key().clone(), e.value().clone())),
813            );
814            snapshot
815                .deletion_tasks
816                .extend(state.deletion_tasks.iter().map(|e| DeletionTask {
817                    task_id: e.key().clone(),
818                    role_name: e.value().clone(),
819                    status: "SUCCEEDED".to_string(),
820                }));
821        }
822
823        serde_json::to_vec(&snapshot).ok()
824    }
825
826    fn restore(&self, data: &[u8]) -> Result<(), String> {
827        let snapshot: IamStateSnapshot = serde_json::from_slice(data).map_err(|e| e.to_string())?;
828
829        // IAM is global — always use the "global" region key.
830        // Derive the account from the ARN of the first entity, or fall back to default.
831        let account_id = snapshot
832            .users
833            .first()
834            .map(|u| {
835                // ARN: arn:aws:iam::{account}:user/{name}
836                let parts: Vec<&str> = u.arn.splitn(6, ':').collect();
837                if parts.len() >= 5 {
838                    parts[4].to_string()
839                } else {
840                    "000000000000".to_string()
841                }
842            })
843            .or_else(|| {
844                snapshot.roles.first().map(|r| {
845                    let parts: Vec<&str> = r.arn.splitn(6, ':').collect();
846                    if parts.len() >= 5 {
847                        parts[4].to_string()
848                    } else {
849                        "000000000000".to_string()
850                    }
851                })
852            })
853            .unwrap_or_else(|| "000000000000".to_string());
854
855        let state = self.store.get(&account_id, IAM_REGION);
856
857        for user in snapshot.users {
858            state.users.insert(user.user_name.clone(), user);
859        }
860        for group in snapshot.groups {
861            state.groups.insert(group.group_name.clone(), group);
862        }
863        for role in snapshot.roles {
864            state.roles.insert(role.role_name.clone(), role);
865        }
866        for policy in snapshot.policies {
867            state.policies.insert(policy.arn.clone(), policy);
868        }
869        for ip in snapshot.instance_profiles {
870            state
871                .instance_profiles
872                .insert(ip.instance_profile_name.clone(), ip);
873        }
874        if !snapshot.account_aliases.is_empty()
875            && let Ok(mut aliases) = state.account_aliases.lock()
876        {
877            *aliases = snapshot.account_aliases;
878        }
879        if let Ok(mut policy) = state.account_password_policy.lock() {
880            *policy = snapshot.account_password_policy;
881        }
882        for provider in snapshot.oidc_providers {
883            state.oidc_providers.insert(provider.arn.clone(), provider);
884        }
885        for provider in snapshot.saml_providers {
886            state.saml_providers.insert(provider.arn.clone(), provider);
887        }
888        for cert in snapshot.server_certificates {
889            state
890                .server_certificates
891                .insert(cert.server_certificate_name.clone(), cert);
892        }
893        for device in snapshot.virtual_mfa_devices {
894            state
895                .virtual_mfa_devices
896                .insert(device.serial_number.clone(), device);
897        }
898        for profile in snapshot.login_profiles {
899            state
900                .login_profiles
901                .insert(profile.user_name.clone(), profile);
902        }
903        for cert in snapshot.signing_certificates {
904            state
905                .signing_certificates
906                .insert(cert.certificate_id.clone(), cert);
907        }
908        for cred in snapshot.service_specific_credentials {
909            state
910                .service_specific_credentials
911                .insert(cred.service_specific_credential_id.clone(), cred);
912        }
913        for (user_name, boundary_arn) in snapshot.user_permissions_boundaries {
914            state
915                .user_permissions_boundaries
916                .insert(user_name, boundary_arn);
917        }
918        for (role_name, boundary_arn) in snapshot.role_permissions_boundaries {
919            state
920                .role_permissions_boundaries
921                .insert(role_name, boundary_arn);
922        }
923        for (key_id, last_used) in snapshot.access_key_last_used {
924            state.access_key_last_used.insert(key_id, last_used);
925        }
926        for task in snapshot.deletion_tasks {
927            state.deletion_tasks.insert(task.task_id, task.role_name);
928        }
929
930        Ok(())
931    }
932}
933
934#[cfg(test)]
935mod root_protection_tests {
936    use super::*;
937    use serde_json::json;
938
939    fn external_ctx() -> RequestContext {
940        RequestContext::new_with_account("iam", "us-east-1", "000000000000")
941    }
942
943    fn internal_ctx() -> RequestContext {
944        RequestContext::internal("iam", "us-east-1", "000000000000")
945    }
946
947    #[tokio::test]
948    async fn external_delete_user_root_is_denied() {
949        let svc = IamService::new();
950        let err = svc
951            .handle("DeleteUser", json!({ "UserName": "root" }), &external_ctx())
952            .await
953            .expect_err("expected AccessDenied");
954        assert_eq!(err.code, "AccessDeniedException");
955    }
956
957    #[tokio::test]
958    async fn external_attach_user_policy_root_is_denied() {
959        let svc = IamService::new();
960        let err = svc
961            .handle(
962                "AttachUserPolicy",
963                json!({
964                    "UserName": "root",
965                    "PolicyArn": "arn:aws:iam::aws:policy/AdministratorAccess",
966                }),
967                &external_ctx(),
968            )
969            .await
970            .expect_err("expected AccessDenied");
971        assert_eq!(err.code, "AccessDeniedException");
972    }
973
974    #[tokio::test]
975    async fn external_create_login_profile_root_is_denied() {
976        let svc = IamService::new();
977        let err = svc
978            .handle(
979                "CreateLoginProfile",
980                json!({ "UserName": "root", "Password": "anything!ABC" }),
981                &external_ctx(),
982            )
983            .await
984            .expect_err("expected AccessDenied");
985        assert_eq!(err.code, "AccessDeniedException");
986    }
987
988    #[tokio::test]
989    async fn internal_create_user_root_is_allowed() {
990        let svc = IamService::new();
991        svc.handle("CreateUser", json!({ "UserName": "root" }), &internal_ctx())
992            .await
993            .expect("internal bootstrap should succeed");
994    }
995
996    #[tokio::test]
997    async fn external_list_users_succeeds_with_root_present() {
998        let svc = IamService::new();
999        svc.handle("CreateUser", json!({ "UserName": "root" }), &internal_ctx())
1000            .await
1001            .unwrap();
1002        // Listing is intentionally not in the protected set: reading
1003        // root metadata is harmless and required for auditing.
1004        let resp = svc.handle("ListUsers", json!({}), &external_ctx()).await;
1005        assert!(resp.is_ok());
1006    }
1007
1008    #[tokio::test]
1009    async fn external_get_user_root_is_allowed() {
1010        let svc = IamService::new();
1011        svc.handle("CreateUser", json!({ "UserName": "root" }), &internal_ctx())
1012            .await
1013            .unwrap();
1014        let resp = svc
1015            .handle("GetUser", json!({ "UserName": "root" }), &external_ctx())
1016            .await;
1017        assert!(resp.is_ok(), "GetUser on root must remain readable");
1018    }
1019
1020    #[tokio::test]
1021    async fn external_mutation_on_non_root_user_is_allowed() {
1022        let svc = IamService::new();
1023        // alice exists and is not root - all mutations should pass
1024        // the root precondition (they may still fail for other
1025        // reasons but the precondition itself must not trigger).
1026        svc.handle(
1027            "CreateUser",
1028            json!({ "UserName": "alice" }),
1029            &external_ctx(),
1030        )
1031        .await
1032        .unwrap();
1033        let resp = svc
1034            .handle(
1035                "DeleteUser",
1036                json!({ "UserName": "alice" }),
1037                &external_ctx(),
1038            )
1039            .await;
1040        assert!(resp.is_ok());
1041    }
1042}