From 06581391bd4c98d5227edf490affb51e25606939 Mon Sep 17 00:00:00 2001 From: gruebel Date: Mon, 28 Oct 2019 23:16:19 +0100 Subject: [PATCH 1/4] Add iam.update_account_password_policy --- IMPLEMENTATION_COVERAGE.md | 2 +- moto/iam/models.py | 81 ++++++++++++++++++++++++++++++++++++++ moto/iam/responses.py | 26 ++++++++++++ tests/test_iam/test_iam.py | 26 ++++++++++++ 4 files changed, 134 insertions(+), 1 deletion(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index cf9f40f8..ed245489 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 4a115999..db233e82 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 d18fac88..9bde665b 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 c4fcda31..dbcdea5f 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' + ) From 65fa8f1a1bdbcbf34f7ec53225a0c97dba2f9b35 Mon Sep 17 00:00:00 2001 From: gruebel Date: Mon, 28 Oct 2019 23:50:17 +0100 Subject: [PATCH 2/4] Add iam.get_account_password_policy --- IMPLEMENTATION_COVERAGE.md | 5 ++-- moto/iam/exceptions.py | 11 ++++++- moto/iam/models.py | 7 +++++ moto/iam/responses.py | 33 +++++++++++++++++++++ tests/test_iam/test_iam.py | 59 ++++++++++++++++++++++++++++++++++++-- 5 files changed, 108 insertions(+), 7 deletions(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index ed245489..184ee1dc 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -3262,8 +3262,7 @@ - [ ] describe_events ## iam -60% implemented -- [ ] add_client_id_to_open_id_connect_provider +61% implemented- [ ] add_client_id_to_open_id_connect_provider - [X] add_role_to_instance_profile - [X] add_user_to_group - [X] attach_group_policy @@ -3317,7 +3316,7 @@ - [ ] generate_service_last_accessed_details - [X] get_access_key_last_used - [X] get_account_authorization_details -- [ ] get_account_password_policy +- [X] get_account_password_policy - [ ] get_account_summary - [ ] get_context_keys_for_custom_policy - [ ] get_context_keys_for_principal_policy diff --git a/moto/iam/exceptions.py b/moto/iam/exceptions.py index b9b0176e..3fc08415 100644 --- a/moto/iam/exceptions.py +++ b/moto/iam/exceptions.py @@ -127,4 +127,13 @@ class InvalidInput(RESTError): code = 400 def __init__(self, message): - super(InvalidInput, self).__init__("InvalidInput", 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 index db233e82..4a4881bf 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -35,6 +35,7 @@ from .exceptions import ( EntityAlreadyExists, ValidationError, InvalidInput, + NoSuchEntity, ) from .utils import ( random_access_key, @@ -1671,5 +1672,11 @@ class IAMBackend(BaseBackend): 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 + iam_backend = IAMBackend() diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 9bde665b..d34e7f59 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -857,6 +857,12 @@ class IamResponse(BaseResponse): 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) + LIST_ENTITIES_FOR_POLICY_TEMPLATE = """ @@ -2196,3 +2202,30 @@ UPDATE_ACCOUNT_PASSWORD_POLICY_TEMPLATE = """7a62c49f-347e-4fc4-9331-6e8eEXAMPLE """ + + +GET_ACCOUNT_PASSWORD_POLICY_TEMPLATE = """ + + + {{ password_policy.allow_users_to_change_password | lower }} + {{ password_policy.expire_passwords | lower }} + {% if password_policy.hard_expiry %} + {{ password_policy.hard_expiry | lower }} + {% endif %} + {% if password_policy.max_password_age %} + {{ password_policy.max_password_age }} + {% endif %} + {{ password_policy.minimum_password_length }} + {% if password_policy.password_reuse_prevention %} + {{ password_policy.password_reuse_prevention }} + {% endif %} + {{ password_policy.require_lowercase_characters | lower }} + {{ password_policy.require_numbers | lower }} + {{ password_policy.require_symbols | lower }} + {{ password_policy.require_uppercase_characters | lower }} + + + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + +""" diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index dbcdea5f..943baa20 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -2200,17 +2200,29 @@ def test_list_open_id_connect_providers(): @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 + MaxPasswordAge=1096, + MinimumPasswordLength=129, + PasswordReusePrevention=25 ).should.throw( ClientError, '3 validation errors detected: ' @@ -2221,3 +2233,44 @@ def test_update_account_password_policy_errors(): '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.' + ) From 89c43820c9adf5020cc2125fa78d567371f546b1 Mon Sep 17 00:00:00 2001 From: gruebel Date: Fri, 1 Nov 2019 07:00:50 +0100 Subject: [PATCH 3/4] Add iam.delete_account_password_policy --- IMPLEMENTATION_COVERAGE.md | 2 +- moto/iam/models.py | 6 ++++++ moto/iam/responses.py | 13 +++++++++++++ tests/test_iam/test_iam.py | 27 +++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 184ee1dc..1b989666 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -3286,7 +3286,7 @@ - [X] deactivate_mfa_device - [X] delete_access_key - [X] delete_account_alias -- [ ] delete_account_password_policy +- [X] delete_account_password_policy - [ ] delete_group - [ ] delete_group_policy - [ ] delete_instance_profile diff --git a/moto/iam/models.py b/moto/iam/models.py index 4a4881bf..c032e1e2 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -1678,5 +1678,11 @@ class IAMBackend(BaseBackend): 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 index d34e7f59..27fbea99 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -863,6 +863,12 @@ class IamResponse(BaseResponse): 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 = """ @@ -2229,3 +2235,10 @@ GET_ACCOUNT_PASSWORD_POLICY_TEMPLATE = """7a62c49f-347e-4fc4-9331-6e8eEXAMPLE """ + + +DELETE_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 943baa20..a05ec862 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -2274,3 +2274,30 @@ def test_get_account_password_policy_errors(): 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.' + ) From 78e2714496ba32206dad1a0584d38591514e2faa Mon Sep 17 00:00:00 2001 From: gruebel Date: Fri, 1 Nov 2019 07:14:03 +0100 Subject: [PATCH 4/4] black format --- IMPLEMENTATION_COVERAGE.md | 10 +++- moto/iam/exceptions.py | 6 +-- moto/iam/models.py | 99 +++++++++++++++++++++++++------------- moto/iam/responses.py | 37 +++++++++----- tests/test_iam/test_iam.py | 83 ++++++++++++++++---------------- 5 files changed, 143 insertions(+), 92 deletions(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 1b989666..2e5f055b 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -2362,7 +2362,7 @@ - [ ] send_ssh_public_key ## ecr -30% implemented +27% implemented - [ ] batch_check_layer_availability - [X] batch_delete_image - [X] batch_get_image @@ -2371,6 +2371,7 @@ - [ ] delete_lifecycle_policy - [X] delete_repository - [ ] delete_repository_policy +- [ ] describe_image_scan_findings - [X] describe_images - [X] describe_repositories - [ ] get_authorization_token @@ -2382,9 +2383,11 @@ - [X] list_images - [ ] list_tags_for_resource - [X] put_image +- [ ] put_image_scanning_configuration - [ ] put_image_tag_mutability - [ ] put_lifecycle_policy - [ ] set_repository_policy +- [ ] start_image_scan - [ ] start_lifecycle_policy_preview - [ ] tag_resource - [ ] untag_resource @@ -2475,6 +2478,7 @@ - [ ] authorize_cache_security_group_ingress - [ ] batch_apply_update_action - [ ] batch_stop_update_action +- [ ] complete_migration - [ ] copy_snapshot - [ ] create_cache_cluster - [ ] create_cache_parameter_group @@ -2516,6 +2520,7 @@ - [ ] remove_tags_from_resource - [ ] reset_cache_parameter_group - [ ] revoke_cache_security_group_ingress +- [ ] start_migration - [ ] test_failover ## elasticbeanstalk @@ -3262,7 +3267,8 @@ - [ ] describe_events ## iam -61% implemented- [ ] add_client_id_to_open_id_connect_provider +62% implemented +- [ ] add_client_id_to_open_id_connect_provider - [X] add_role_to_instance_profile - [X] add_user_to_group - [X] attach_group_policy diff --git a/moto/iam/exceptions.py b/moto/iam/exceptions.py index 3fc08415..1d0f3ca0 100644 --- a/moto/iam/exceptions.py +++ b/moto/iam/exceptions.py @@ -127,13 +127,11 @@ class InvalidInput(RESTError): code = 400 def __init__(self, message): - super(InvalidInput, self).__init__( - 'InvalidInput', message) + super(InvalidInput, self).__init__("InvalidInput", message) class NoSuchEntity(RESTError): code = 404 def __init__(self, message): - super(NoSuchEntity, self).__init__( - 'NoSuchEntity', message) + super(NoSuchEntity, self).__init__("NoSuchEntity", message) diff --git a/moto/iam/models.py b/moto/iam/models.py index c032e1e2..2a76e912 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -654,12 +654,22 @@ 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): + 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._validate( + max_password_age, minimum_password_length, password_reuse_prevention + ) self.allow_users_to_change_password = allow_change_password self.hard_expiry = hard_expiry @@ -675,35 +685,41 @@ class AccountPasswordPolicy(BaseModel): 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): + 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' - )) + 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' - )) + 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._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, + constraint=constraint, key=key, value=value, ) def _raise_errors(self): @@ -713,9 +729,11 @@ class AccountPasswordPolicy(BaseModel): errors = "; ".join(self._errors) self._errors = [] # reset collected errors - raise ValidationError('{count} validation error{plural} detected: {errors}'.format( - count=count, plural=plural, errors=errors, - )) + raise ValidationError( + "{count} validation error{plural} detected: {errors}".format( + count=count, plural=plural, errors=errors, + ) + ) class IAMBackend(BaseBackend): @@ -1657,9 +1675,18 @@ 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): + 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, @@ -1669,18 +1696,24 @@ class IAMBackend(BaseBackend): require_lowercase_characters, require_numbers, require_symbols, - require_uppercase_characters + 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)) + 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.') + raise NoSuchEntity( + "The account policy with name PasswordPolicy cannot be found." + ) self.account_password_policy = None diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 27fbea99..08fe13dc 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -839,20 +839,33 @@ class IamResponse(BaseResponse): 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) + 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) + 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() diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index a05ec862..e0d8fdb8 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -2199,45 +2199,45 @@ def test_list_open_id_connect_providers(): @mock_iam def test_update_account_password_policy(): - client = boto3.client('iam', region_name='us-east-1') + 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 - }) + 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 = boto3.client("iam", region_name="us-east-1") client.update_account_password_policy.when.called_with( - MaxPasswordAge=1096, - MinimumPasswordLength=129, - PasswordReusePrevention=25 + MaxPasswordAge=1096, MinimumPasswordLength=129, PasswordReusePrevention=25 ).should.throw( ClientError, - '3 validation errors detected: ' + "3 validation errors detected: " 'Value "129" at "minimumPasswordLength" failed to satisfy constraint: ' - 'Member must have value less than or equal to 128; ' + "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; ' + "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' + "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 = boto3.client("iam", region_name="us-east-1") client.update_account_password_policy( AllowUsersToChangePassword=True, HardExpiry=True, @@ -2247,57 +2247,58 @@ def test_get_account_password_policy(): RequireLowercaseCharacters=True, RequireNumbers=True, RequireSymbols=True, - RequireUppercaseCharacters=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 - }) + + 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 = 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.' + "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 = 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) + 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.' + "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 = 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.' + ClientError, "The account policy with name PasswordPolicy cannot be found." )