Merge pull request #2525 from edekadigital/add-iam-account-password-policy

Add iam account password policy
This commit is contained in:
Steve Pulec 2019-11-04 22:44:04 -06:00 committed by GitHub
commit ad6b73b38e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 336 additions and 5 deletions

View File

@ -2362,7 +2362,7 @@
- [ ] send_ssh_public_key - [ ] send_ssh_public_key
## ecr ## ecr
30% implemented 27% implemented
- [ ] batch_check_layer_availability - [ ] batch_check_layer_availability
- [X] batch_delete_image - [X] batch_delete_image
- [X] batch_get_image - [X] batch_get_image
@ -2371,6 +2371,7 @@
- [ ] delete_lifecycle_policy - [ ] delete_lifecycle_policy
- [X] delete_repository - [X] delete_repository
- [ ] delete_repository_policy - [ ] delete_repository_policy
- [ ] describe_image_scan_findings
- [X] describe_images - [X] describe_images
- [X] describe_repositories - [X] describe_repositories
- [ ] get_authorization_token - [ ] get_authorization_token
@ -2382,9 +2383,11 @@
- [X] list_images - [X] list_images
- [ ] list_tags_for_resource - [ ] list_tags_for_resource
- [X] put_image - [X] put_image
- [ ] put_image_scanning_configuration
- [ ] put_image_tag_mutability - [ ] put_image_tag_mutability
- [ ] put_lifecycle_policy - [ ] put_lifecycle_policy
- [ ] set_repository_policy - [ ] set_repository_policy
- [ ] start_image_scan
- [ ] start_lifecycle_policy_preview - [ ] start_lifecycle_policy_preview
- [ ] tag_resource - [ ] tag_resource
- [ ] untag_resource - [ ] untag_resource
@ -2475,6 +2478,7 @@
- [ ] authorize_cache_security_group_ingress - [ ] authorize_cache_security_group_ingress
- [ ] batch_apply_update_action - [ ] batch_apply_update_action
- [ ] batch_stop_update_action - [ ] batch_stop_update_action
- [ ] complete_migration
- [ ] copy_snapshot - [ ] copy_snapshot
- [ ] create_cache_cluster - [ ] create_cache_cluster
- [ ] create_cache_parameter_group - [ ] create_cache_parameter_group
@ -2516,6 +2520,7 @@
- [ ] remove_tags_from_resource - [ ] remove_tags_from_resource
- [ ] reset_cache_parameter_group - [ ] reset_cache_parameter_group
- [ ] revoke_cache_security_group_ingress - [ ] revoke_cache_security_group_ingress
- [ ] start_migration
- [ ] test_failover - [ ] test_failover
## elasticbeanstalk ## elasticbeanstalk
@ -3262,7 +3267,7 @@
- [ ] describe_events - [ ] describe_events
## iam ## iam
60% implemented 62% implemented
- [ ] add_client_id_to_open_id_connect_provider - [ ] add_client_id_to_open_id_connect_provider
- [X] add_role_to_instance_profile - [X] add_role_to_instance_profile
- [X] add_user_to_group - [X] add_user_to_group
@ -3287,7 +3292,7 @@
- [X] deactivate_mfa_device - [X] deactivate_mfa_device
- [X] delete_access_key - [X] delete_access_key
- [X] delete_account_alias - [X] delete_account_alias
- [ ] delete_account_password_policy - [X] delete_account_password_policy
- [ ] delete_group - [ ] delete_group
- [ ] delete_group_policy - [ ] delete_group_policy
- [ ] delete_instance_profile - [ ] delete_instance_profile
@ -3317,7 +3322,7 @@
- [ ] generate_service_last_accessed_details - [ ] generate_service_last_accessed_details
- [X] get_access_key_last_used - [X] get_access_key_last_used
- [X] get_account_authorization_details - [X] get_account_authorization_details
- [ ] get_account_password_policy - [X] get_account_password_policy
- [ ] get_account_summary - [ ] get_account_summary
- [ ] get_context_keys_for_custom_policy - [ ] get_context_keys_for_custom_policy
- [ ] get_context_keys_for_principal_policy - [ ] get_context_keys_for_principal_policy
@ -3387,7 +3392,7 @@
- [X] untag_role - [X] untag_role
- [ ] untag_user - [ ] untag_user
- [X] update_access_key - [X] update_access_key
- [ ] update_account_password_policy - [X] update_account_password_policy
- [ ] update_assume_role_policy - [ ] update_assume_role_policy
- [ ] update_group - [ ] update_group
- [X] update_login_profile - [X] update_login_profile

View File

@ -128,3 +128,10 @@ class InvalidInput(RESTError):
def __init__(self, message): 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)

View File

@ -35,6 +35,7 @@ from .exceptions import (
EntityAlreadyExists, EntityAlreadyExists,
ValidationError, ValidationError,
InvalidInput, InvalidInput,
NoSuchEntity,
) )
from .utils import ( from .utils import (
random_access_key, random_access_key,
@ -652,6 +653,89 @@ 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): class IAMBackend(BaseBackend):
def __init__(self): def __init__(self):
self.instance_profiles = {} self.instance_profiles = {}
@ -666,6 +750,7 @@ class IAMBackend(BaseBackend):
self.open_id_providers = {} self.open_id_providers = {}
self.policy_arn_regex = re.compile(r"^arn:aws:iam::[0-9]*:policy/.*$") self.policy_arn_regex = re.compile(r"^arn:aws:iam::[0-9]*:policy/.*$")
self.virtual_mfa_devices = {} self.virtual_mfa_devices = {}
self.account_password_policy = None
super(IAMBackend, self).__init__() super(IAMBackend, self).__init__()
def _init_managed_policies(self): def _init_managed_policies(self):
@ -1590,5 +1675,47 @@ class IAMBackend(BaseBackend):
def list_open_id_connect_providers(self): def list_open_id_connect_providers(self):
return list(self.open_id_providers.keys()) 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() iam_backend = IAMBackend()

View File

@ -838,6 +838,50 @@ class IamResponse(BaseResponse):
template = self.response_template(LIST_OPEN_ID_CONNECT_PROVIDERS_TEMPLATE) template = self.response_template(LIST_OPEN_ID_CONNECT_PROVIDERS_TEMPLATE)
return template.render(open_id_provider_arns=open_id_provider_arns) 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> LIST_ENTITIES_FOR_POLICY_TEMPLATE = """<ListEntitiesForPolicyResponse>
<ListEntitiesForPolicyResult> <ListEntitiesForPolicyResult>
@ -2170,3 +2214,44 @@ LIST_OPEN_ID_CONNECT_PROVIDERS_TEMPLATE = """<ListOpenIDConnectProvidersResponse
<RequestId>de2c0228-4f63-11e4-aefa-bfd6aEXAMPLE</RequestId> <RequestId>de2c0228-4f63-11e4-aefa-bfd6aEXAMPLE</RequestId>
</ResponseMetadata> </ResponseMetadata>
</ListOpenIDConnectProvidersResponse>""" </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>"""

View File

@ -2195,3 +2195,110 @@ def test_list_open_id_connect_providers():
sorted(response["OpenIDConnectProviderList"], key=lambda i: i["Arn"]).should.equal( 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}] [{"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."
)