diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index cf9f40f80..ed245489d 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -3387,7 +3387,7 @@ - [X] untag_role - [ ] untag_user - [X] update_access_key -- [ ] update_account_password_policy +- [X] update_account_password_policy - [ ] update_assume_role_policy - [ ] update_group - [X] update_login_profile diff --git a/moto/iam/models.py b/moto/iam/models.py index 4a115999c..db233e82b 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -652,6 +652,71 @@ class User(BaseModel): ) +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 +731,7 @@ class IAMBackend(BaseBackend): 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 +1656,20 @@ class IAMBackend(BaseBackend): 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 + ) + iam_backend = IAMBackend() diff --git a/moto/iam/responses.py b/moto/iam/responses.py index d18fac88d..9bde665b2 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -838,6 +838,25 @@ class IamResponse(BaseResponse): 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() + LIST_ENTITIES_FOR_POLICY_TEMPLATE = """ @@ -2170,3 +2189,10 @@ LIST_OPEN_ID_CONNECT_PROVIDERS_TEMPLATE = """de2c0228-4f63-11e4-aefa-bfd6aEXAMPLE """ + + +UPDATE_ACCOUNT_PASSWORD_POLICY_TEMPLATE = """ + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + +""" diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index c4fcda317..dbcdea5f2 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -2195,3 +2195,29 @@ 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() + + +@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' + )