Repository: getmoto/moto

Issue:
NotImplementedError: update_account_password_policy
I am mocking account management processes using moto and I'm getting the following error:

`The update_account_password_policy action has not been implemented.` 

The code i'm running looks like this: 
``` 
iam_client = get_aws_session("iam","create_account_lambda","OrganizationAccountAccessRole",account,region)
    
###### update the password policy

    
response = iam_client.update_account_password_policy(
        MinimumPasswordLength=100,
        RequireSymbols=True,
        RequireNumbers=True,
        RequireUppercaseCharacters=True,
        RequireLowercaseCharacters=True,
        AllowUsersToChangePassword=True,
        MaxPasswordAge=90,
        PasswordReusePrevention=24,
        HardExpiry=False
)
```

I'm using moto version 1.3.13. 
I can see that in the implementation coverage that it has not yet been implemented. Any idea if or when it will be?



Discussion:
Thanks; setting this as a feature request.

As a general rule, new services/endpoints are implemented by those who need them. PRs welcome from anyone with time.

Resolving patch:
diff --git a/moto/iam/exceptions.py b/moto/iam/exceptions.py
--- a/moto/iam/exceptions.py
+++ b/moto/iam/exceptions.py
@@ -128,3 +128,10 @@ class InvalidInput(RESTError):
 
     def __init__(self, message):
         super(InvalidInput, self).__init__("InvalidInput", message)
+
+
+class NoSuchEntity(RESTError):
+    code = 404
+
+    def __init__(self, message):
+        super(NoSuchEntity, self).__init__("NoSuchEntity", message)
diff --git a/moto/iam/models.py b/moto/iam/models.py
--- a/moto/iam/models.py
+++ b/moto/iam/models.py
@@ -35,6 +35,7 @@
     EntityAlreadyExists,
     ValidationError,
     InvalidInput,
+    NoSuchEntity,
 )
 from .utils import (
     random_access_key,
@@ -652,6 +653,89 @@ def to_csv(self):
         )
 
 
+class AccountPasswordPolicy(BaseModel):
+    def __init__(
+        self,
+        allow_change_password,
+        hard_expiry,
+        max_password_age,
+        minimum_password_length,
+        password_reuse_prevention,
+        require_lowercase_characters,
+        require_numbers,
+        require_symbols,
+        require_uppercase_characters,
+    ):
+        self._errors = []
+        self._validate(
+            max_password_age, minimum_password_length, password_reuse_prevention
+        )
+
+        self.allow_users_to_change_password = allow_change_password
+        self.hard_expiry = hard_expiry
+        self.max_password_age = max_password_age
+        self.minimum_password_length = minimum_password_length
+        self.password_reuse_prevention = password_reuse_prevention
+        self.require_lowercase_characters = require_lowercase_characters
+        self.require_numbers = require_numbers
+        self.require_symbols = require_symbols
+        self.require_uppercase_characters = require_uppercase_characters
+
+    @property
+    def expire_passwords(self):
+        return True if self.max_password_age and self.max_password_age > 0 else False
+
+    def _validate(
+        self, max_password_age, minimum_password_length, password_reuse_prevention
+    ):
+        if minimum_password_length > 128:
+            self._errors.append(
+                self._format_error(
+                    key="minimumPasswordLength",
+                    value=minimum_password_length,
+                    constraint="Member must have value less than or equal to 128",
+                )
+            )
+
+        if password_reuse_prevention and password_reuse_prevention > 24:
+            self._errors.append(
+                self._format_error(
+                    key="passwordReusePrevention",
+                    value=password_reuse_prevention,
+                    constraint="Member must have value less than or equal to 24",
+                )
+            )
+
+        if max_password_age and max_password_age > 1095:
+            self._errors.append(
+                self._format_error(
+                    key="maxPasswordAge",
+                    value=max_password_age,
+                    constraint="Member must have value less than or equal to 1095",
+                )
+            )
+
+        self._raise_errors()
+
+    def _format_error(self, key, value, constraint):
+        return 'Value "{value}" at "{key}" failed to satisfy constraint: {constraint}'.format(
+            constraint=constraint, key=key, value=value,
+        )
+
+    def _raise_errors(self):
+        if self._errors:
+            count = len(self._errors)
+            plural = "s" if len(self._errors) > 1 else ""
+            errors = "; ".join(self._errors)
+            self._errors = []  # reset collected errors
+
+            raise ValidationError(
+                "{count} validation error{plural} detected: {errors}".format(
+                    count=count, plural=plural, errors=errors,
+                )
+            )
+
+
 class IAMBackend(BaseBackend):
     def __init__(self):
         self.instance_profiles = {}
@@ -666,6 +750,7 @@ def __init__(self):
         self.open_id_providers = {}
         self.policy_arn_regex = re.compile(r"^arn:aws:iam::[0-9]*:policy/.*$")
         self.virtual_mfa_devices = {}
+        self.account_password_policy = None
         super(IAMBackend, self).__init__()
 
     def _init_managed_policies(self):
@@ -1590,5 +1675,47 @@ def get_open_id_connect_provider(self, arn):
     def list_open_id_connect_providers(self):
         return list(self.open_id_providers.keys())
 
+    def update_account_password_policy(
+        self,
+        allow_change_password,
+        hard_expiry,
+        max_password_age,
+        minimum_password_length,
+        password_reuse_prevention,
+        require_lowercase_characters,
+        require_numbers,
+        require_symbols,
+        require_uppercase_characters,
+    ):
+        self.account_password_policy = AccountPasswordPolicy(
+            allow_change_password,
+            hard_expiry,
+            max_password_age,
+            minimum_password_length,
+            password_reuse_prevention,
+            require_lowercase_characters,
+            require_numbers,
+            require_symbols,
+            require_uppercase_characters,
+        )
+
+    def get_account_password_policy(self):
+        if not self.account_password_policy:
+            raise NoSuchEntity(
+                "The Password Policy with domain name {} cannot be found.".format(
+                    ACCOUNT_ID
+                )
+            )
+
+        return self.account_password_policy
+
+    def delete_account_password_policy(self):
+        if not self.account_password_policy:
+            raise NoSuchEntity(
+                "The account policy with name PasswordPolicy cannot be found."
+            )
+
+        self.account_password_policy = None
+
 
 iam_backend = IAMBackend()
diff --git a/moto/iam/responses.py b/moto/iam/responses.py
--- a/moto/iam/responses.py
+++ b/moto/iam/responses.py
@@ -838,6 +838,50 @@ def list_open_id_connect_providers(self):
         template = self.response_template(LIST_OPEN_ID_CONNECT_PROVIDERS_TEMPLATE)
         return template.render(open_id_provider_arns=open_id_provider_arns)
 
+    def update_account_password_policy(self):
+        allow_change_password = self._get_bool_param(
+            "AllowUsersToChangePassword", False
+        )
+        hard_expiry = self._get_bool_param("HardExpiry")
+        max_password_age = self._get_int_param("MaxPasswordAge")
+        minimum_password_length = self._get_int_param("MinimumPasswordLength", 6)
+        password_reuse_prevention = self._get_int_param("PasswordReusePrevention")
+        require_lowercase_characters = self._get_bool_param(
+            "RequireLowercaseCharacters", False
+        )
+        require_numbers = self._get_bool_param("RequireNumbers", False)
+        require_symbols = self._get_bool_param("RequireSymbols", False)
+        require_uppercase_characters = self._get_bool_param(
+            "RequireUppercaseCharacters", False
+        )
+
+        iam_backend.update_account_password_policy(
+            allow_change_password,
+            hard_expiry,
+            max_password_age,
+            minimum_password_length,
+            password_reuse_prevention,
+            require_lowercase_characters,
+            require_numbers,
+            require_symbols,
+            require_uppercase_characters,
+        )
+
+        template = self.response_template(UPDATE_ACCOUNT_PASSWORD_POLICY_TEMPLATE)
+        return template.render()
+
+    def get_account_password_policy(self):
+        account_password_policy = iam_backend.get_account_password_policy()
+
+        template = self.response_template(GET_ACCOUNT_PASSWORD_POLICY_TEMPLATE)
+        return template.render(password_policy=account_password_policy)
+
+    def delete_account_password_policy(self):
+        iam_backend.delete_account_password_policy()
+
+        template = self.response_template(DELETE_ACCOUNT_PASSWORD_POLICY_TEMPLATE)
+        return template.render()
+
 
 LIST_ENTITIES_FOR_POLICY_TEMPLATE = """<ListEntitiesForPolicyResponse>
  <ListEntitiesForPolicyResult>
@@ -2170,3 +2214,44 @@ def list_open_id_connect_providers(self):
     <RequestId>de2c0228-4f63-11e4-aefa-bfd6aEXAMPLE</RequestId>
   </ResponseMetadata>
 </ListOpenIDConnectProvidersResponse>"""
+
+
+UPDATE_ACCOUNT_PASSWORD_POLICY_TEMPLATE = """<UpdateAccountPasswordPolicyResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
+ <ResponseMetadata>
+    <RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
+ </ResponseMetadata>
+</UpdateAccountPasswordPolicyResponse>"""
+
+
+GET_ACCOUNT_PASSWORD_POLICY_TEMPLATE = """<GetAccountPasswordPolicyResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
+  <GetAccountPasswordPolicyResult>
+    <PasswordPolicy>
+      <AllowUsersToChangePassword>{{ password_policy.allow_users_to_change_password | lower }}</AllowUsersToChangePassword>
+      <ExpirePasswords>{{ password_policy.expire_passwords | lower }}</ExpirePasswords>
+      {% if password_policy.hard_expiry %}
+      <HardExpiry>{{ password_policy.hard_expiry | lower }}</HardExpiry>
+      {% endif %}
+      {% if password_policy.max_password_age %}
+      <MaxPasswordAge>{{ password_policy.max_password_age }}</MaxPasswordAge>
+      {% endif %}
+      <MinimumPasswordLength>{{ password_policy.minimum_password_length }}</MinimumPasswordLength>
+      {% if password_policy.password_reuse_prevention %}
+      <PasswordReusePrevention>{{ password_policy.password_reuse_prevention }}</PasswordReusePrevention>
+      {% endif %}
+      <RequireLowercaseCharacters>{{ password_policy.require_lowercase_characters | lower }}</RequireLowercaseCharacters>
+      <RequireNumbers>{{ password_policy.require_numbers | lower }}</RequireNumbers>
+      <RequireSymbols>{{ password_policy.require_symbols | lower }}</RequireSymbols>
+      <RequireUppercaseCharacters>{{ password_policy.require_uppercase_characters | lower }}</RequireUppercaseCharacters>
+    </PasswordPolicy>
+  </GetAccountPasswordPolicyResult>
+  <ResponseMetadata>
+    <RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
+  </ResponseMetadata>
+</GetAccountPasswordPolicyResponse>"""
+
+
+DELETE_ACCOUNT_PASSWORD_POLICY_TEMPLATE = """<DeleteAccountPasswordPolicyResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
+  <ResponseMetadata>
+    <RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
+  </ResponseMetadata>
+</DeleteAccountPasswordPolicyResponse>"""


Test patch:
diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py
--- a/tests/test_iam/test_iam.py
+++ b/tests/test_iam/test_iam.py
@@ -2195,3 +2195,110 @@ def test_list_open_id_connect_providers():
     sorted(response["OpenIDConnectProviderList"], key=lambda i: i["Arn"]).should.equal(
         [{"Arn": open_id_arn_1}, {"Arn": open_id_arn_2}, {"Arn": open_id_arn_3}]
     )
+
+
+@mock_iam
+def test_update_account_password_policy():
+    client = boto3.client("iam", region_name="us-east-1")
+
+    client.update_account_password_policy()
+
+    response = client.get_account_password_policy()
+    response["PasswordPolicy"].should.equal(
+        {
+            "AllowUsersToChangePassword": False,
+            "ExpirePasswords": False,
+            "MinimumPasswordLength": 6,
+            "RequireLowercaseCharacters": False,
+            "RequireNumbers": False,
+            "RequireSymbols": False,
+            "RequireUppercaseCharacters": False,
+        }
+    )
+
+
+@mock_iam
+def test_update_account_password_policy_errors():
+    client = boto3.client("iam", region_name="us-east-1")
+
+    client.update_account_password_policy.when.called_with(
+        MaxPasswordAge=1096, MinimumPasswordLength=129, PasswordReusePrevention=25
+    ).should.throw(
+        ClientError,
+        "3 validation errors detected: "
+        'Value "129" at "minimumPasswordLength" failed to satisfy constraint: '
+        "Member must have value less than or equal to 128; "
+        'Value "25" at "passwordReusePrevention" failed to satisfy constraint: '
+        "Member must have value less than or equal to 24; "
+        'Value "1096" at "maxPasswordAge" failed to satisfy constraint: '
+        "Member must have value less than or equal to 1095",
+    )
+
+
+@mock_iam
+def test_get_account_password_policy():
+    client = boto3.client("iam", region_name="us-east-1")
+    client.update_account_password_policy(
+        AllowUsersToChangePassword=True,
+        HardExpiry=True,
+        MaxPasswordAge=60,
+        MinimumPasswordLength=10,
+        PasswordReusePrevention=3,
+        RequireLowercaseCharacters=True,
+        RequireNumbers=True,
+        RequireSymbols=True,
+        RequireUppercaseCharacters=True,
+    )
+
+    response = client.get_account_password_policy()
+
+    response["PasswordPolicy"].should.equal(
+        {
+            "AllowUsersToChangePassword": True,
+            "ExpirePasswords": True,
+            "HardExpiry": True,
+            "MaxPasswordAge": 60,
+            "MinimumPasswordLength": 10,
+            "PasswordReusePrevention": 3,
+            "RequireLowercaseCharacters": True,
+            "RequireNumbers": True,
+            "RequireSymbols": True,
+            "RequireUppercaseCharacters": True,
+        }
+    )
+
+
+@mock_iam
+def test_get_account_password_policy_errors():
+    client = boto3.client("iam", region_name="us-east-1")
+
+    client.get_account_password_policy.when.called_with().should.throw(
+        ClientError,
+        "The Password Policy with domain name 123456789012 cannot be found.",
+    )
+
+
+@mock_iam
+def test_delete_account_password_policy():
+    client = boto3.client("iam", region_name="us-east-1")
+    client.update_account_password_policy()
+
+    response = client.get_account_password_policy()
+
+    response.should.have.key("PasswordPolicy").which.should.be.a(dict)
+
+    client.delete_account_password_policy()
+
+    client.get_account_password_policy.when.called_with().should.throw(
+        ClientError,
+        "The Password Policy with domain name 123456789012 cannot be found.",
+    )
+
+
+@mock_iam
+def test_delete_account_password_policy_errors():
+    client = boto3.client("iam", region_name="us-east-1")
+
+    client.delete_account_password_policy.when.called_with().should.throw(
+        ClientError, "The account policy with name PasswordPolicy cannot be found."
+    )