diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index c026ef601..3d8338cfc 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -171,7 +171,7 @@ - [ ] update_webhook ## apigateway -24% implemented +25% implemented - [ ] create_api_key - [ ] create_authorizer - [ ] create_base_path_mapping @@ -204,7 +204,7 @@ - [ ] delete_request_validator - [X] delete_resource - [X] delete_rest_api -- [ ] delete_stage +- [X] delete_stage - [X] delete_usage_plan - [X] delete_usage_plan_key - [ ] delete_vpc_link @@ -687,12 +687,17 @@ ## ce 0% implemented - [ ] get_cost_and_usage +- [ ] get_cost_and_usage_with_resources - [ ] get_cost_forecast - [ ] get_dimension_values - [ ] get_reservation_coverage - [ ] get_reservation_purchase_recommendation - [ ] get_reservation_utilization - [ ] get_rightsizing_recommendation +- [ ] get_savings_plans_coverage +- [ ] get_savings_plans_purchase_recommendation +- [ ] get_savings_plans_utilization +- [ ] get_savings_plans_utilization_details - [ ] get_tags - [ ] get_usage_forecast @@ -701,6 +706,7 @@ - [ ] associate_phone_number_with_user - [ ] associate_phone_numbers_with_voice_connector - [ ] associate_phone_numbers_with_voice_connector_group +- [ ] batch_create_room_membership - [ ] batch_delete_phone_number - [ ] batch_suspend_user - [ ] batch_unsuspend_user @@ -709,11 +715,15 @@ - [ ] create_account - [ ] create_bot - [ ] create_phone_number_order +- [ ] create_room +- [ ] create_room_membership - [ ] create_voice_connector - [ ] create_voice_connector_group - [ ] delete_account - [ ] delete_events_configuration - [ ] delete_phone_number +- [ ] delete_room +- [ ] delete_room_membership - [ ] delete_voice_connector - [ ] delete_voice_connector_group - [ ] delete_voice_connector_origination @@ -731,6 +741,7 @@ - [ ] get_phone_number - [ ] get_phone_number_order - [ ] get_phone_number_settings +- [ ] get_room - [ ] get_user - [ ] get_user_settings - [ ] get_voice_connector @@ -745,6 +756,8 @@ - [ ] list_bots - [ ] list_phone_number_orders - [ ] list_phone_numbers +- [ ] list_room_memberships +- [ ] list_rooms - [ ] list_users - [ ] list_voice_connector_groups - [ ] list_voice_connector_termination_credentials @@ -766,6 +779,8 @@ - [ ] update_global_settings - [ ] update_phone_number - [ ] update_phone_number_settings +- [ ] update_room +- [ ] update_room_membership - [ ] update_user - [ ] update_user_settings - [ ] update_voice_connector @@ -1003,6 +1018,7 @@ - [ ] delete_suggester - [ ] describe_analysis_schemes - [ ] describe_availability_options +- [ ] describe_domain_endpoint_options - [ ] describe_domains - [ ] describe_expressions - [ ] describe_index_fields @@ -1012,6 +1028,7 @@ - [ ] index_documents - [ ] list_domain_names - [ ] update_availability_options +- [ ] update_domain_endpoint_options - [ ] update_scaling_parameters - [ ] update_service_access_policies @@ -1028,9 +1045,11 @@ - [ ] delete_trail - [ ] describe_trails - [ ] get_event_selectors +- [ ] get_trail - [ ] get_trail_status - [ ] list_public_keys - [ ] list_tags +- [ ] list_trails - [ ] lookup_events - [ ] put_event_selectors - [ ] remove_tags @@ -1252,6 +1271,22 @@ - [ ] update_team_member - [ ] update_user_profile +## codestar-notifications +0% implemented +- [ ] create_notification_rule +- [ ] delete_notification_rule +- [ ] delete_target +- [ ] describe_notification_rule +- [ ] list_event_types +- [ ] list_notification_rules +- [ ] list_tags_for_resource +- [ ] list_targets +- [ ] subscribe +- [ ] tag_resource +- [ ] unsubscribe +- [ ] untag_resource +- [ ] update_notification_rule + ## cognito-identity 28% implemented - [X] create_identity_pool @@ -1545,10 +1580,13 @@ - [ ] list_queues - [ ] list_routing_profiles - [ ] list_security_profiles +- [ ] list_tags_for_resource - [ ] list_user_hierarchy_groups - [ ] list_users - [ ] start_outbound_voice_contact - [ ] stop_contact +- [ ] tag_resource +- [ ] untag_resource - [ ] update_contact_attributes - [ ] update_user_hierarchy - [ ] update_user_identity_info @@ -1563,6 +1601,31 @@ - [ ] modify_report_definition - [ ] put_report_definition +## dataexchange +0% implemented +- [ ] cancel_job +- [ ] create_data_set +- [ ] create_job +- [ ] create_revision +- [ ] delete_asset +- [ ] delete_data_set +- [ ] delete_revision +- [ ] get_asset +- [ ] get_data_set +- [ ] get_job +- [ ] get_revision +- [ ] list_data_set_revisions +- [ ] list_data_sets +- [ ] list_jobs +- [ ] list_revision_assets +- [ ] list_tags_for_resource +- [ ] start_job +- [ ] tag_resource +- [ ] untag_resource +- [ ] update_asset +- [ ] update_data_set +- [ ] update_revision + ## datapipeline 42% implemented - [X] activate_pipeline @@ -1586,17 +1649,17 @@ - [ ] validate_pipeline_definition ## datasync -0% implemented -- [ ] cancel_task_execution +22% implemented +- [X] cancel_task_execution - [ ] create_agent - [ ] create_location_efs - [ ] create_location_nfs - [ ] create_location_s3 - [ ] create_location_smb -- [ ] create_task +- [X] create_task - [ ] delete_agent -- [ ] delete_location -- [ ] delete_task +- [X] delete_location +- [X] delete_task - [ ] describe_agent - [ ] describe_location_efs - [ ] describe_location_nfs @@ -1609,11 +1672,11 @@ - [ ] list_tags_for_resource - [ ] list_task_executions - [ ] list_tasks -- [ ] start_task_execution +- [X] start_task_execution - [ ] tag_resource - [ ] untag_resource - [ ] update_agent -- [ ] update_task +- [X] update_task ## dax 0% implemented @@ -1799,6 +1862,9 @@ - [ ] delete_lifecycle_policy - [ ] get_lifecycle_policies - [ ] get_lifecycle_policy +- [ ] list_tags_for_resource +- [ ] tag_resource +- [ ] untag_resource - [ ] update_lifecycle_policy ## dms @@ -2217,8 +2283,8 @@ - [X] describe_volumes - [ ] describe_volumes_modifications - [X] describe_vpc_attribute -- [ ] describe_vpc_classic_link -- [ ] describe_vpc_classic_link_dns_support +- [X] describe_vpc_classic_link +- [X] describe_vpc_classic_link_dns_support - [ ] describe_vpc_endpoint_connection_notifications - [ ] describe_vpc_endpoint_connections - [ ] describe_vpc_endpoint_service_configurations @@ -2237,8 +2303,8 @@ - [ ] disable_ebs_encryption_by_default - [ ] disable_transit_gateway_route_table_propagation - [ ] disable_vgw_route_propagation -- [ ] disable_vpc_classic_link -- [ ] disable_vpc_classic_link_dns_support +- [X] disable_vpc_classic_link +- [X] disable_vpc_classic_link_dns_support - [X] disassociate_address - [ ] disassociate_client_vpn_target_network - [ ] disassociate_iam_instance_profile @@ -2250,8 +2316,8 @@ - [ ] enable_transit_gateway_route_table_propagation - [ ] enable_vgw_route_propagation - [ ] enable_volume_io -- [ ] enable_vpc_classic_link -- [ ] enable_vpc_classic_link_dns_support +- [X] enable_vpc_classic_link +- [X] enable_vpc_classic_link_dns_support - [ ] export_client_vpn_client_certificate_revocation_list - [ ] export_client_vpn_client_configuration - [ ] export_image @@ -2461,16 +2527,22 @@ ## eks 0% implemented - [ ] create_cluster +- [ ] create_nodegroup - [ ] delete_cluster +- [ ] delete_nodegroup - [ ] describe_cluster +- [ ] describe_nodegroup - [ ] describe_update - [ ] list_clusters +- [ ] list_nodegroups - [ ] list_tags_for_resource - [ ] list_updates - [ ] tag_resource - [ ] untag_resource - [ ] update_cluster_config - [ ] update_cluster_version +- [ ] update_nodegroup_config +- [ ] update_nodegroup_version ## elasticache 0% implemented @@ -3217,6 +3289,7 @@ - [ ] create_filter - [ ] create_ip_set - [ ] create_members +- [ ] create_publishing_destination - [ ] create_sample_findings - [ ] create_threat_intel_set - [ ] decline_invitations @@ -3225,7 +3298,9 @@ - [ ] delete_invitations - [ ] delete_ip_set - [ ] delete_members +- [ ] delete_publishing_destination - [ ] delete_threat_intel_set +- [ ] describe_publishing_destination - [ ] disassociate_from_master_account - [ ] disassociate_members - [ ] get_detector @@ -3244,6 +3319,7 @@ - [ ] list_invitations - [ ] list_ip_sets - [ ] list_members +- [ ] list_publishing_destinations - [ ] list_tags_for_resource - [ ] list_threat_intel_sets - [ ] start_monitoring_members @@ -3255,6 +3331,7 @@ - [ ] update_filter - [ ] update_findings_feedback - [ ] update_ip_set +- [ ] update_publishing_destination - [ ] update_threat_intel_set ## health @@ -3267,7 +3344,7 @@ - [ ] describe_events ## iam -62% implemented +65% implemented - [ ] add_client_id_to_open_id_connect_provider - [X] add_role_to_instance_profile - [X] add_user_to_group @@ -3293,7 +3370,7 @@ - [X] delete_access_key - [X] delete_account_alias - [X] delete_account_password_policy -- [ ] delete_group +- [X] delete_group - [ ] delete_group_policy - [ ] delete_instance_profile - [X] delete_login_profile @@ -3323,7 +3400,7 @@ - [X] get_access_key_last_used - [X] get_account_authorization_details - [X] get_account_password_policy -- [ ] get_account_summary +- [X] get_account_summary - [ ] get_context_keys_for_custom_policy - [ ] get_context_keys_for_principal_policy - [X] get_credential_report @@ -3405,7 +3482,7 @@ - [X] update_signing_certificate - [ ] update_ssh_public_key - [X] update_user -- [ ] upload_server_certificate +- [X] upload_server_certificate - [X] upload_signing_certificate - [ ] upload_ssh_public_key @@ -3459,7 +3536,7 @@ - [ ] update_assessment_target ## iot -23% implemented +22% implemented - [ ] accept_certificate_transfer - [ ] add_thing_to_billing_group - [X] add_thing_to_thing_group @@ -3544,11 +3621,13 @@ - [X] detach_thing_principal - [ ] disable_topic_rule - [ ] enable_topic_rule +- [ ] get_cardinality - [ ] get_effective_policies - [ ] get_indexing_configuration - [ ] get_job_document - [ ] get_logging_options - [ ] get_ota_update +- [ ] get_percentiles - [X] get_policy - [ ] get_policy_version - [ ] get_registration_code @@ -4295,6 +4374,15 @@ - [ ] reject_invitation - [ ] vote_on_proposal +## marketplace-catalog +0% implemented +- [ ] cancel_change_set +- [ ] describe_change_set +- [ ] describe_entity +- [ ] list_change_sets +- [ ] list_entities +- [ ] start_change_set + ## marketplace-entitlement 0% implemented - [ ] get_entitlements @@ -4773,6 +4861,7 @@ ## personalize 0% implemented +- [ ] create_batch_inference_job - [ ] create_campaign - [ ] create_dataset - [ ] create_dataset_group @@ -4788,6 +4877,7 @@ - [ ] delete_schema - [ ] delete_solution - [ ] describe_algorithm +- [ ] describe_batch_inference_job - [ ] describe_campaign - [ ] describe_dataset - [ ] describe_dataset_group @@ -4799,6 +4889,7 @@ - [ ] describe_solution - [ ] describe_solution_version - [ ] get_solution_metrics +- [ ] list_batch_inference_jobs - [ ] list_campaigns - [ ] list_dataset_groups - [ ] list_dataset_import_jobs @@ -4831,6 +4922,7 @@ - [ ] create_email_template - [ ] create_export_job - [ ] create_import_job +- [ ] create_journey - [ ] create_push_template - [ ] create_segment - [ ] create_sms_template @@ -4847,6 +4939,7 @@ - [ ] delete_endpoint - [ ] delete_event_stream - [ ] delete_gcm_channel +- [ ] delete_journey - [ ] delete_push_template - [ ] delete_segment - [ ] delete_sms_channel @@ -4879,6 +4972,10 @@ - [ ] get_gcm_channel - [ ] get_import_job - [ ] get_import_jobs +- [ ] get_journey +- [ ] get_journey_date_range_kpi +- [ ] get_journey_execution_activity_metrics +- [ ] get_journey_execution_metrics - [ ] get_push_template - [ ] get_segment - [ ] get_segment_export_jobs @@ -4890,6 +4987,7 @@ - [ ] get_sms_template - [ ] get_user_endpoints - [ ] get_voice_channel +- [ ] list_journeys - [ ] list_tags_for_resource - [ ] list_templates - [ ] phone_number_validate @@ -4913,6 +5011,8 @@ - [ ] update_endpoint - [ ] update_endpoints_batch - [ ] update_gcm_channel +- [ ] update_journey +- [ ] update_journey_state - [ ] update_push_template - [ ] update_segment - [ ] update_sms_channel @@ -5661,6 +5761,17 @@ 0% implemented - [ ] invoke_endpoint +## savingsplans +0% implemented +- [ ] create_savings_plan +- [ ] describe_savings_plan_rates +- [ ] describe_savings_plans +- [ ] describe_savings_plans_offering_rates +- [ ] describe_savings_plans_offerings +- [ ] list_tags_for_resource +- [ ] tag_resource +- [ ] untag_resource + ## sdb 0% implemented - [ ] batch_delete_attributes @@ -5954,6 +6065,51 @@ - [X] verify_email_address - [X] verify_email_identity +## sesv2 +0% implemented +- [ ] create_configuration_set +- [ ] create_configuration_set_event_destination +- [ ] create_dedicated_ip_pool +- [ ] create_deliverability_test_report +- [ ] create_email_identity +- [ ] delete_configuration_set +- [ ] delete_configuration_set_event_destination +- [ ] delete_dedicated_ip_pool +- [ ] delete_email_identity +- [ ] get_account +- [ ] get_blacklist_reports +- [ ] get_configuration_set +- [ ] get_configuration_set_event_destinations +- [ ] get_dedicated_ip +- [ ] get_dedicated_ips +- [ ] get_deliverability_dashboard_options +- [ ] get_deliverability_test_report +- [ ] get_domain_deliverability_campaign +- [ ] get_domain_statistics_report +- [ ] get_email_identity +- [ ] list_configuration_sets +- [ ] list_dedicated_ip_pools +- [ ] list_deliverability_test_reports +- [ ] list_domain_deliverability_campaigns +- [ ] list_email_identities +- [ ] list_tags_for_resource +- [ ] put_account_dedicated_ip_warmup_attributes +- [ ] put_account_sending_attributes +- [ ] put_configuration_set_delivery_options +- [ ] put_configuration_set_reputation_options +- [ ] put_configuration_set_sending_options +- [ ] put_configuration_set_tracking_options +- [ ] put_dedicated_ip_in_pool +- [ ] put_dedicated_ip_warmup_attributes +- [ ] put_deliverability_dashboard_option +- [ ] put_email_identity_dkim_attributes +- [ ] put_email_identity_feedback_attributes +- [ ] put_email_identity_mail_from_attributes +- [ ] send_email +- [ ] tag_resource +- [ ] untag_resource +- [ ] update_configuration_set_event_destination + ## shield 0% implemented - [ ] associate_drt_log_bucket @@ -5984,8 +6140,11 @@ - [ ] list_signing_jobs - [ ] list_signing_platforms - [ ] list_signing_profiles +- [ ] list_tags_for_resource - [ ] put_signing_profile - [ ] start_signing_job +- [ ] tag_resource +- [ ] untag_resource ## sms 0% implemented @@ -6111,7 +6270,7 @@ - [X] untag_queue ## ssm -10% implemented +11% implemented - [X] add_tags_to_resource - [ ] cancel_command - [ ] cancel_maintenance_window_execution @@ -6184,7 +6343,7 @@ - [ ] get_ops_item - [ ] get_ops_summary - [X] get_parameter -- [ ] get_parameter_history +- [X] get_parameter_history - [X] get_parameters - [X] get_parameters_by_path - [ ] get_patch_baseline @@ -6233,6 +6392,19 @@ - [ ] update_patch_baseline - [ ] update_service_setting +## sso +0% implemented +- [ ] get_role_credentials +- [ ] list_account_roles +- [ ] list_accounts +- [ ] logout + +## sso-oidc +0% implemented +- [ ] create_token +- [ ] register_client +- [ ] start_device_authorization + ## stepfunctions 36% implemented - [ ] create_activity @@ -6742,6 +6914,7 @@ - [ ] delete_ip_group - [ ] delete_tags - [ ] delete_workspace_image +- [ ] deregister_workspace_directory - [ ] describe_account - [ ] describe_account_modifications - [ ] describe_client_properties @@ -6758,10 +6931,14 @@ - [ ] list_available_management_cidr_ranges - [ ] modify_account - [ ] modify_client_properties +- [ ] modify_selfservice_permissions +- [ ] modify_workspace_access_properties +- [ ] modify_workspace_creation_properties - [ ] modify_workspace_properties - [ ] modify_workspace_state - [ ] reboot_workspaces - [ ] rebuild_workspaces +- [ ] register_workspace_directory - [ ] restore_workspace - [ ] revoke_ip_rules - [ ] start_workspaces diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index fe5c24c53..3bd186aca 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -46,8 +46,9 @@ try: except ImportError: from backports.tempfile import TemporaryDirectory - -_stderr_regex = re.compile(r"START|END|REPORT RequestId: .*") +# The lambci container is returning a special escape character for the "RequestID" fields. Unicode 033: +# _stderr_regex = re.compile(r"START|END|REPORT RequestId: .*") +_stderr_regex = re.compile(r"\033\[\d+.*") _orig_adapter_send = requests.adapters.HTTPAdapter.send docker_3 = docker.__version__[0] >= "3" @@ -444,7 +445,7 @@ class LambdaFunction(BaseModel): if exit_code != 0: raise Exception("lambda invoke failed output: {}".format(output)) - # strip out RequestId lines + # strip out RequestId lines (TODO: This will return an additional '\n' in the response) output = os.linesep.join( [ line diff --git a/moto/batch/models.py b/moto/batch/models.py index 5c7fb4739..ab52db54c 100644 --- a/moto/batch/models.py +++ b/moto/batch/models.py @@ -624,7 +624,7 @@ class BatchBackend(BaseBackend): def get_job_definition(self, identifier): """ - Get job defintiion by name or ARN + Get job definitions by name or ARN :param identifier: Name or ARN :type identifier: str @@ -643,7 +643,7 @@ class BatchBackend(BaseBackend): def get_job_definitions(self, identifier): """ - Get job defintiion by name or ARN + Get job definitions by name or ARN :param identifier: Name or ARN :type identifier: str @@ -934,7 +934,7 @@ class BatchBackend(BaseBackend): self.ecs_backend.delete_cluster(compute_env.ecs_name) if compute_env.env_type == "MANAGED": - # Delete compute envrionment + # Delete compute environment instance_ids = [instance.id for instance in compute_env.instances] self.ec2_backend.terminate_instances(instance_ids) @@ -1195,7 +1195,7 @@ class BatchBackend(BaseBackend): depends_on=None, container_overrides=None, ): - # TODO parameters, retries (which is a dict raw from request), job dependancies and container overrides are ignored for now + # TODO parameters, retries (which is a dict raw from request), job dependencies and container overrides are ignored for now # Look for job definition job_def = self.get_job_definition(job_def_id) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index ccc7c7a37..afb23dc80 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -2444,6 +2444,7 @@ class VPC(TaggedEC2Resource): self.instance_tenancy = instance_tenancy self.is_default = "true" if is_default else "false" self.enable_dns_support = "true" + self.classic_link_enabled = "false" # This attribute is set to 'true' only for default VPCs # or VPCs created using the wizard of the VPC console self.enable_dns_hostnames = "true" if is_default else "false" @@ -2540,6 +2541,32 @@ class VPC(TaggedEC2Resource): self.cidr_block_association_set[association_id] = association_set return association_set + def enable_vpc_classic_link(self): + # Check if current cidr block doesn't fall within the 10.0.0.0/8 block, excluding 10.0.0.0/16 and 10.1.0.0/16. + # Doesn't check any route tables, maybe something for in the future? + # See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/vpc-classiclink.html#classiclink-limitations + network_address = ipaddress.ip_network(self.cidr_block).network_address + if ( + network_address not in ipaddress.ip_network("10.0.0.0/8") + or network_address in ipaddress.ip_network("10.0.0.0/16") + or network_address in ipaddress.ip_network("10.1.0.0/16") + ): + self.classic_link_enabled = "true" + + return self.classic_link_enabled + + def disable_vpc_classic_link(self): + self.classic_link_enabled = "false" + return self.classic_link_enabled + + def enable_vpc_classic_link_dns_support(self): + self.classic_link_dns_supported = "true" + return self.classic_link_dns_supported + + def disable_vpc_classic_link_dns_support(self): + self.classic_link_dns_supported = "false" + return self.classic_link_dns_supported + def disassociate_vpc_cidr_block(self, association_id): if self.cidr_block == self.cidr_block_association_set.get( association_id, {} @@ -2670,6 +2697,22 @@ class VPCBackend(object): else: raise InvalidParameterValueError(attr_name) + def enable_vpc_classic_link(self, vpc_id): + vpc = self.get_vpc(vpc_id) + return vpc.enable_vpc_classic_link() + + def disable_vpc_classic_link(self, vpc_id): + vpc = self.get_vpc(vpc_id) + return vpc.disable_vpc_classic_link() + + def enable_vpc_classic_link_dns_support(self, vpc_id): + vpc = self.get_vpc(vpc_id) + return vpc.enable_vpc_classic_link_dns_support() + + def disable_vpc_classic_link_dns_support(self, vpc_id): + vpc = self.get_vpc(vpc_id) + return vpc.disable_vpc_classic_link_dns_support() + def modify_vpc_attribute(self, vpc_id, attr_name, attr_value): vpc = self.get_vpc(vpc_id) if attr_name in ("enable_dns_support", "enable_dns_hostnames"): diff --git a/moto/ec2/responses/vpcs.py b/moto/ec2/responses/vpcs.py index 1773e4cc8..0fd198378 100644 --- a/moto/ec2/responses/vpcs.py +++ b/moto/ec2/responses/vpcs.py @@ -5,6 +5,13 @@ from moto.ec2.utils import filters_from_querystring class VPCs(BaseResponse): + def _get_doc_date(self): + return ( + "2013-10-15" + if "Boto/" in self.headers.get("user-agent", "") + else "2016-11-15" + ) + def create_vpc(self): cidr_block = self._get_param("CidrBlock") instance_tenancy = self._get_param("InstanceTenancy", if_none="default") @@ -16,11 +23,7 @@ class VPCs(BaseResponse): instance_tenancy, amazon_provided_ipv6_cidr_block=amazon_provided_ipv6_cidr_blocks, ) - doc_date = ( - "2013-10-15" - if "Boto/" in self.headers.get("user-agent", "") - else "2016-11-15" - ) + doc_date = self._get_doc_date() template = self.response_template(CREATE_VPC_RESPONSE) return template.render(vpc=vpc, doc_date=doc_date) @@ -50,6 +53,64 @@ class VPCs(BaseResponse): template = self.response_template(DESCRIBE_VPC_ATTRIBUTE_RESPONSE) return template.render(vpc_id=vpc_id, attribute=attribute, value=value) + def describe_vpc_classic_link_dns_support(self): + vpc_ids = self._get_multi_param("VpcIds") + filters = filters_from_querystring(self.querystring) + vpcs = self.ec2_backend.get_all_vpcs(vpc_ids=vpc_ids, filters=filters) + doc_date = self._get_doc_date() + template = self.response_template( + DESCRIBE_VPC_CLASSIC_LINK_DNS_SUPPORT_RESPONSE + ) + return template.render(vpcs=vpcs, doc_date=doc_date) + + def enable_vpc_classic_link_dns_support(self): + vpc_id = self._get_param("VpcId") + classic_link_dns_supported = self.ec2_backend.enable_vpc_classic_link_dns_support( + vpc_id=vpc_id + ) + doc_date = self._get_doc_date() + template = self.response_template(ENABLE_VPC_CLASSIC_LINK_DNS_SUPPORT_RESPONSE) + return template.render( + classic_link_dns_supported=classic_link_dns_supported, doc_date=doc_date + ) + + def disable_vpc_classic_link_dns_support(self): + vpc_id = self._get_param("VpcId") + classic_link_dns_supported = self.ec2_backend.disable_vpc_classic_link_dns_support( + vpc_id=vpc_id + ) + doc_date = self._get_doc_date() + template = self.response_template(DISABLE_VPC_CLASSIC_LINK_DNS_SUPPORT_RESPONSE) + return template.render( + classic_link_dns_supported=classic_link_dns_supported, doc_date=doc_date + ) + + def describe_vpc_classic_link(self): + vpc_ids = self._get_multi_param("VpcId") + filters = filters_from_querystring(self.querystring) + vpcs = self.ec2_backend.get_all_vpcs(vpc_ids=vpc_ids, filters=filters) + doc_date = self._get_doc_date() + template = self.response_template(DESCRIBE_VPC_CLASSIC_LINK_RESPONSE) + return template.render(vpcs=vpcs, doc_date=doc_date) + + def enable_vpc_classic_link(self): + vpc_id = self._get_param("VpcId") + classic_link_enabled = self.ec2_backend.enable_vpc_classic_link(vpc_id=vpc_id) + doc_date = self._get_doc_date() + template = self.response_template(ENABLE_VPC_CLASSIC_LINK_RESPONSE) + return template.render( + classic_link_enabled=classic_link_enabled, doc_date=doc_date + ) + + def disable_vpc_classic_link(self): + vpc_id = self._get_param("VpcId") + classic_link_enabled = self.ec2_backend.disable_vpc_classic_link(vpc_id=vpc_id) + doc_date = self._get_doc_date() + template = self.response_template(DISABLE_VPC_CLASSIC_LINK_RESPONSE) + return template.render( + classic_link_enabled=classic_link_enabled, doc_date=doc_date + ) + def modify_vpc_attribute(self): vpc_id = self._get_param("VpcId") @@ -149,6 +210,56 @@ CREATE_VPC_RESPONSE = """ """ +DESCRIBE_VPC_CLASSIC_LINK_DNS_SUPPORT_RESPONSE = """ + + 7a62c442-3484-4f42-9342-6942EXAMPLE + + {% for vpc in vpcs %} + + {{ vpc.id }} + {{ vpc.classic_link_dns_supported }} + + {% endfor %} + +""" + +ENABLE_VPC_CLASSIC_LINK_DNS_SUPPORT_RESPONSE = """ + + 7a62c442-3484-4f42-9342-6942EXAMPLE + {{ classic_link_dns_supported }} +""" + +DISABLE_VPC_CLASSIC_LINK_DNS_SUPPORT_RESPONSE = """ + + 7a62c442-3484-4f42-9342-6942EXAMPLE + {{ classic_link_dns_supported }} +""" + +DESCRIBE_VPC_CLASSIC_LINK_RESPONSE = """ + + 7a62c442-3484-4f42-9342-6942EXAMPLE + + {% for vpc in vpcs %} + + {{ vpc.id }} + {{ vpc.classic_link_enabled }} + + {% endfor %} + +""" + +ENABLE_VPC_CLASSIC_LINK_RESPONSE = """ + + 7a62c442-3484-4f42-9342-6942EXAMPLE + {{ classic_link_enabled }} +""" + +DISABLE_VPC_CLASSIC_LINK_RESPONSE = """ + + 7a62c442-3484-4f42-9342-6942EXAMPLE + {{ classic_link_enabled }} +""" + DESCRIBE_VPCS_RESPONSE = """ 7a62c442-3484-4f42-9342-6942EXAMPLE diff --git a/moto/iam/models.py b/moto/iam/models.py index b64c9402f..73d58b996 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals import base64 +import hashlib import os import random import string @@ -475,6 +476,20 @@ class AccessKey(BaseModel): raise UnformattedGetAttTemplateException() +class SshPublicKey(BaseModel): + def __init__(self, user_name, ssh_public_key_body): + self.user_name = user_name + self.ssh_public_key_body = ssh_public_key_body + self.ssh_public_key_id = "APKA" + random_access_key() + self.fingerprint = hashlib.md5(ssh_public_key_body.encode()).hexdigest() + self.status = "Active" + self.upload_date = datetime.utcnow() + + @property + def uploaded_iso_8601(self): + return iso_8601_datetime_without_milliseconds(self.upload_date) + + class Group(BaseModel): def __init__(self, name, path="/"): self.name = name @@ -536,6 +551,7 @@ class User(BaseModel): self.policies = {} self.managed_policies = {} self.access_keys = [] + self.ssh_public_keys = [] self.password = None self.password_reset_required = False self.signing_certificates = {} @@ -605,6 +621,33 @@ class User(BaseModel): "The Access Key with id {0} cannot be found".format(access_key_id) ) + def upload_ssh_public_key(self, ssh_public_key_body): + pubkey = SshPublicKey(self.name, ssh_public_key_body) + self.ssh_public_keys.append(pubkey) + return pubkey + + def get_ssh_public_key(self, ssh_public_key_id): + for key in self.ssh_public_keys: + if key.ssh_public_key_id == ssh_public_key_id: + return key + else: + raise IAMNotFoundException( + "The SSH Public Key with id {0} cannot be found".format( + ssh_public_key_id + ) + ) + + def get_all_ssh_public_keys(self): + return self.ssh_public_keys + + def update_ssh_public_key(self, ssh_public_key_id, status): + key = self.get_ssh_public_key(ssh_public_key_id) + key.status = status + + def delete_ssh_public_key(self, ssh_public_key_id): + key = self.get_ssh_public_key(ssh_public_key_id) + self.ssh_public_keys.remove(key) + def get_cfn_attribute(self, attribute_name): from moto.cloudformation.exceptions import UnformattedGetAttTemplateException @@ -736,6 +779,134 @@ class AccountPasswordPolicy(BaseModel): ) +class AccountSummary(BaseModel): + def __init__(self, iam_backend): + self._iam_backend = iam_backend + + self._group_policy_size_quota = 5120 + self._instance_profiles_quota = 1000 + self._groups_per_user_quota = 10 + self._attached_policies_per_user_quota = 10 + self._policies_quota = 1500 + self._account_mfa_enabled = 0 # Haven't found any information being able to activate MFA for the root account programmatically + self._access_keys_per_user_quota = 2 + self._assume_role_policy_size_quota = 2048 + self._policy_versions_in_use_quota = 10000 + self._global_endpoint_token_version = ( + 1 # ToDo: Implement set_security_token_service_preferences() + ) + self._versions_per_policy_quota = 5 + self._attached_policies_per_group_quota = 10 + self._policy_size_quota = 6144 + self._account_signing_certificates_present = 0 # valid values: 0 | 1 + self._users_quota = 5000 + self._server_certificates_quota = 20 + self._user_policy_size_quota = 2048 + self._roles_quota = 1000 + self._signing_certificates_per_user_quota = 2 + self._role_policy_size_quota = 10240 + self._attached_policies_per_role_quota = 10 + self._account_access_keys_present = 0 # valid values: 0 | 1 + self._groups_quota = 300 + + @property + def summary_map(self): + return { + "GroupPolicySizeQuota": self._group_policy_size_quota, + "InstanceProfilesQuota": self._instance_profiles_quota, + "Policies": self._policies, + "GroupsPerUserQuota": self._groups_per_user_quota, + "InstanceProfiles": self._instance_profiles, + "AttachedPoliciesPerUserQuota": self._attached_policies_per_user_quota, + "Users": self._users, + "PoliciesQuota": self._policies_quota, + "Providers": self._providers, + "AccountMFAEnabled": self._account_mfa_enabled, + "AccessKeysPerUserQuota": self._access_keys_per_user_quota, + "AssumeRolePolicySizeQuota": self._assume_role_policy_size_quota, + "PolicyVersionsInUseQuota": self._policy_versions_in_use_quota, + "GlobalEndpointTokenVersion": self._global_endpoint_token_version, + "VersionsPerPolicyQuota": self._versions_per_policy_quota, + "AttachedPoliciesPerGroupQuota": self._attached_policies_per_group_quota, + "PolicySizeQuota": self._policy_size_quota, + "Groups": self._groups, + "AccountSigningCertificatesPresent": self._account_signing_certificates_present, + "UsersQuota": self._users_quota, + "ServerCertificatesQuota": self._server_certificates_quota, + "MFADevices": self._mfa_devices, + "UserPolicySizeQuota": self._user_policy_size_quota, + "PolicyVersionsInUse": self._policy_versions_in_use, + "ServerCertificates": self._server_certificates, + "Roles": self._roles, + "RolesQuota": self._roles_quota, + "SigningCertificatesPerUserQuota": self._signing_certificates_per_user_quota, + "MFADevicesInUse": self._mfa_devices_in_use, + "RolePolicySizeQuota": self._role_policy_size_quota, + "AttachedPoliciesPerRoleQuota": self._attached_policies_per_role_quota, + "AccountAccessKeysPresent": self._account_access_keys_present, + "GroupsQuota": self._groups_quota, + } + + @property + def _groups(self): + return len(self._iam_backend.groups) + + @property + def _instance_profiles(self): + return len(self._iam_backend.instance_profiles) + + @property + def _mfa_devices(self): + # Don't know, if hardware devices are also counted here + return len(self._iam_backend.virtual_mfa_devices) + + @property + def _mfa_devices_in_use(self): + devices = 0 + + for user in self._iam_backend.users.values(): + devices += len(user.mfa_devices) + + return devices + + @property + def _policies(self): + customer_policies = [ + policy + for policy in self._iam_backend.managed_policies + if not policy.startswith("arn:aws:iam::aws:policy") + ] + return len(customer_policies) + + @property + def _policy_versions_in_use(self): + attachments = 0 + + for policy in self._iam_backend.managed_policies.values(): + attachments += policy.attachment_count + + return attachments + + @property + def _providers(self): + providers = len(self._iam_backend.saml_providers) + len( + self._iam_backend.open_id_providers + ) + return providers + + @property + def _roles(self): + return len(self._iam_backend.roles) + + @property + def _server_certificates(self): + return len(self._iam_backend.certificates) + + @property + def _users(self): + return len(self._iam_backend.users) + + class IAMBackend(BaseBackend): def __init__(self): self.instance_profiles = {} @@ -751,6 +922,7 @@ class IAMBackend(BaseBackend): self.policy_arn_regex = re.compile(r"^arn:aws:iam::[0-9]*:policy/.*$") self.virtual_mfa_devices = {} self.account_password_policy = None + self.account_summary = AccountSummary(self) super(IAMBackend, self).__init__() def _init_managed_policies(self): @@ -820,7 +992,7 @@ class IAMBackend(BaseBackend): ) if policy.arn in self.managed_policies: raise EntityAlreadyExists( - "A policy called {} already exists. Duplicate names are not allowed.".format( + "A policy called {0} already exists. Duplicate names are not allowed.".format( policy_name ) ) @@ -898,6 +1070,10 @@ class IAMBackend(BaseBackend): permissions_boundary ), ) + if [role for role in self.get_roles() if role.name == role_name]: + raise EntityAlreadyExists( + "Role with name {0} already exists.".format(role_name) + ) clean_tags = self._tag_verification(tags) role = Role( @@ -1158,7 +1334,7 @@ class IAMBackend(BaseBackend): def get_all_server_certs(self, marker=None): return self.certificates.values() - def upload_server_cert( + def upload_server_certificate( self, cert_name, cert_body, private_key, cert_chain=None, path=None ): certificate_id = random_resource_id() @@ -1451,6 +1627,26 @@ class IAMBackend(BaseBackend): user = self.get_user(user_name) user.delete_access_key(access_key_id) + def upload_ssh_public_key(self, user_name, ssh_public_key_body): + user = self.get_user(user_name) + return user.upload_ssh_public_key(ssh_public_key_body) + + def get_ssh_public_key(self, user_name, ssh_public_key_id): + user = self.get_user(user_name) + return user.get_ssh_public_key(ssh_public_key_id) + + def get_all_ssh_public_keys(self, user_name): + user = self.get_user(user_name) + return user.get_all_ssh_public_keys() + + def update_ssh_public_key(self, user_name, ssh_public_key_id, status): + user = self.get_user(user_name) + return user.update_ssh_public_key(ssh_public_key_id, status) + + def delete_ssh_public_key(self, user_name, ssh_public_key_id): + user = self.get_user(user_name) + return user.delete_ssh_public_key(ssh_public_key_id) + def enable_mfa_device( self, user_name, serial_number, authentication_code_1, authentication_code_2 ): @@ -1737,5 +1933,8 @@ class IAMBackend(BaseBackend): self.account_password_policy = None + def get_account_summary(self): + return self.account_summary + iam_backend = IAMBackend() diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 4bd1aa80c..ea14bef0f 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -351,7 +351,7 @@ class IamResponse(BaseResponse): private_key = self._get_param("PrivateKey") cert_chain = self._get_param("CertificateName") - cert = iam_backend.upload_server_cert( + cert = iam_backend.upload_server_certificate( cert_name, cert_body, private_key, cert_chain=cert_chain, path=path ) template = self.response_template(UPLOAD_CERT_TEMPLATE) @@ -590,6 +590,46 @@ class IamResponse(BaseResponse): template = self.response_template(GENERIC_EMPTY_TEMPLATE) return template.render(name="DeleteAccessKey") + def upload_ssh_public_key(self): + user_name = self._get_param("UserName") + ssh_public_key_body = self._get_param("SSHPublicKeyBody") + + key = iam_backend.upload_ssh_public_key(user_name, ssh_public_key_body) + template = self.response_template(UPLOAD_SSH_PUBLIC_KEY_TEMPLATE) + return template.render(key=key) + + def get_ssh_public_key(self): + user_name = self._get_param("UserName") + ssh_public_key_id = self._get_param("SSHPublicKeyId") + + key = iam_backend.get_ssh_public_key(user_name, ssh_public_key_id) + template = self.response_template(GET_SSH_PUBLIC_KEY_TEMPLATE) + return template.render(key=key) + + def list_ssh_public_keys(self): + user_name = self._get_param("UserName") + + keys = iam_backend.get_all_ssh_public_keys(user_name) + template = self.response_template(LIST_SSH_PUBLIC_KEYS_TEMPLATE) + return template.render(keys=keys) + + def update_ssh_public_key(self): + user_name = self._get_param("UserName") + ssh_public_key_id = self._get_param("SSHPublicKeyId") + status = self._get_param("Status") + + iam_backend.update_ssh_public_key(user_name, ssh_public_key_id, status) + template = self.response_template(UPDATE_SSH_PUBLIC_KEY_TEMPLATE) + return template.render() + + def delete_ssh_public_key(self): + user_name = self._get_param("UserName") + ssh_public_key_id = self._get_param("SSHPublicKeyId") + + iam_backend.delete_ssh_public_key(user_name, ssh_public_key_id) + template = self.response_template(DELETE_SSH_PUBLIC_KEY_TEMPLATE) + return template.render() + def deactivate_mfa_device(self): user_name = self._get_param("UserName") serial_number = self._get_param("SerialNumber") @@ -888,6 +928,12 @@ class IamResponse(BaseResponse): template = self.response_template(DELETE_ACCOUNT_PASSWORD_POLICY_TEMPLATE) return template.render() + def get_account_summary(self): + account_summary = iam_backend.get_account_summary() + + template = self.response_template(GET_ACCOUNT_SUMMARY_TEMPLATE) + return template.render(summary_map=account_summary.summary_map) + LIST_ENTITIES_FOR_POLICY_TEMPLATE = """ @@ -1690,6 +1736,73 @@ GET_ACCESS_KEY_LAST_USED_TEMPLATE = """ """ +UPLOAD_SSH_PUBLIC_KEY_TEMPLATE = """ + + + {{ key.user_name }} + {{ key.ssh_public_key_body }} + {{ key.ssh_public_key_id }} + {{ key.fingerprint }} + {{ key.status }} + {{ key.uploaded_iso_8601 }} + + + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + +""" + +GET_SSH_PUBLIC_KEY_TEMPLATE = """ + + + {{ key.user_name }} + {{ key.ssh_public_key_body }} + {{ key.ssh_public_key_id }} + {{ key.fingerprint }} + {{ key.status }} + {{ key.uploaded_iso_8601 }} + + + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + +""" + +LIST_SSH_PUBLIC_KEYS_TEMPLATE = """ + + + {% for key in keys %} + + {{ key.user_name }} + {{ key.ssh_public_key_id }} + {{ key.status }} + {{ key.uploaded_iso_8601 }} + + {% endfor %} + + false + + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + +""" + +UPDATE_SSH_PUBLIC_KEY_TEMPLATE = """ + + + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + +""" + +DELETE_SSH_PUBLIC_KEY_TEMPLATE = """ + + + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + +""" + CREDENTIAL_REPORT_GENERATING = """ @@ -2261,3 +2374,20 @@ DELETE_ACCOUNT_PASSWORD_POLICY_TEMPLATE = """7a62c49f-347e-4fc4-9331-6e8eEXAMPLE """ + + +GET_ACCOUNT_SUMMARY_TEMPLATE = """ + + + {% for key, value in summary_map.items() %} + + {{ key }} + {{ value }} + + {% endfor %} + + + + 85cb9b90-ac28-11e4-a88d-97964EXAMPLE + +""" diff --git a/moto/packages/httpretty/core.py b/moto/packages/httpretty/core.py index 0c9635e79..83bd19237 100644 --- a/moto/packages/httpretty/core.py +++ b/moto/packages/httpretty/core.py @@ -125,7 +125,7 @@ class HTTPrettyRequest(BaseHTTPRequestHandler, BaseClass): internal `parse_request` method. It also replaces the `rfile` and `wfile` attributes with StringIO - instances so that we garantee that it won't make any I/O, neighter + instances so that we guarantee that it won't make any I/O, neighter for writing nor reading. It has some convenience attributes: diff --git a/moto/server.py b/moto/server.py index bbc309fe2..92fe6f229 100644 --- a/moto/server.py +++ b/moto/server.py @@ -190,7 +190,7 @@ def create_backend_app(service): index = 2 while endpoint in backend_app.view_functions: # HACK: Sometimes we map the same view to multiple url_paths. Flask - # requries us to have different names. + # requires us to have different names. endpoint = original_endpoint + str(index) index += 1 diff --git a/moto/ses/models.py b/moto/ses/models.py index 353d6f4b7..eacdd8458 100644 --- a/moto/ses/models.py +++ b/moto/ses/models.py @@ -147,7 +147,7 @@ class SESBackend(BaseBackend): def __type_of_message__(self, destinations): """Checks the destination for any special address that could indicate delivery, - complaint or bounce like in SES simualtor""" + complaint or bounce like in SES simulator""" alladdress = ( destinations.get("ToAddresses", []) + destinations.get("CcAddresses", []) diff --git a/moto/sns/models.py b/moto/sns/models.py index 949234c27..8b125358d 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -227,7 +227,7 @@ class Subscription(BaseModel): return False for attribute_values in attribute_values: - # Even the offical documentation states a 5 digits of accuracy after the decimal point for numerics, in reality it is 6 + # Even the official documentation states a 5 digits of accuracy after the decimal point for numerics, in reality it is 6 # https://docs.aws.amazon.com/sns/latest/dg/sns-subscription-filter-policies.html#subscription-filter-policy-constraints if int(attribute_values * 1000000) == int(rule * 1000000): return True @@ -573,7 +573,7 @@ class SNSBackend(BaseBackend): combinations = 1 for rules in six.itervalues(value): combinations *= len(rules) - # Even the offical documentation states the total combination of values must not exceed 100, in reality it is 150 + # Even the official documentation states the total combination of values must not exceed 100, in reality it is 150 # https://docs.aws.amazon.com/sns/latest/dg/sns-subscription-filter-policies.html#subscription-filter-policy-constraints if combinations > 150: raise SNSInvalidParameter( diff --git a/moto/sns/responses.py b/moto/sns/responses.py index 23964c54a..c2eb3e7c3 100644 --- a/moto/sns/responses.py +++ b/moto/sns/responses.py @@ -77,7 +77,7 @@ class SNSResponse(BaseResponse): transform_value = value["StringValue"] elif "BinaryValue" in value: transform_value = value["BinaryValue"] - if not transform_value: + if transform_value == "": raise InvalidParameterValue( "The message attribute '{0}' must contain non-empty " "message attribute value for message attribute " diff --git a/moto/sqs/models.py b/moto/sqs/models.py index e975c1bae..ca3d41f38 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -761,7 +761,7 @@ class SQSBackend(BaseBackend): new_messages = [] for message in queue._messages: - # Only delete message if it is not visible and the reciept_handle + # Only delete message if it is not visible and the receipt_handle # matches. if message.receipt_handle == receipt_handle: queue.pending_messages.remove(message) diff --git a/moto/swf/models/workflow_execution.py b/moto/swf/models/workflow_execution.py index fca780a41..4d91b1f6f 100644 --- a/moto/swf/models/workflow_execution.py +++ b/moto/swf/models/workflow_execution.py @@ -430,7 +430,7 @@ class WorkflowExecution(BaseModel): ) def fail(self, event_id, details=None, reason=None): - # TODO: implement lenght constraints on details/reason + # TODO: implement length constraints on details/reason self.execution_status = "CLOSED" self.close_status = "FAILED" self.close_timestamp = unix_time() diff --git a/setup.py b/setup.py index a45b2b589..97a6341ff 100755 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ install_requires = [ "werkzeug", "PyYAML>=5.1", "pytz", - "python-dateutil<3.0.0,>=2.1", + "python-dateutil<2.8.1,>=2.1", "python-jose<4.0.0", "mock", "docker>=2.5.1", diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py index 306deeea4..6b3d489ca 100644 --- a/tests/test_awslambda/test_lambda.py +++ b/tests/test_awslambda/test_lambda.py @@ -162,7 +162,7 @@ if settings.TEST_SERVER_MODE: conn = boto3.client("lambda", "us-west-2") conn.create_function( FunctionName="testFunction", - Runtime="python2.7", + Runtime="python3.7", Role="test-iam-role", Handler="lambda_function.lambda_handler", Code={"ZipFile": get_test_zip_file2()}, @@ -184,18 +184,20 @@ if settings.TEST_SERVER_MODE: vol.id, vol.state, vol.size, - json.dumps(in_data), + json.dumps(in_data).replace( + " ", "" + ), # Makes the tests pass as the result is missing the whitespace ) log_result = base64.b64decode(result["LogResult"]).decode("utf-8") - # fix for running under travis (TODO: investigate why it has an extra newline) + # The Docker lambda invocation will return an additional '\n', so need to replace it: log_result = log_result.replace("\n\n", "\n") log_result.should.equal(msg) payload = result["Payload"].read().decode("utf-8") - # fix for running under travis (TODO: investigate why it has an extra newline) + # The Docker lambda invocation will return an additional '\n', so need to replace it: payload = payload.replace("\n\n", "\n") payload.should.equal(msg) diff --git a/tests/test_core/test_auth.py b/tests/test_core/test_auth.py index c5e043ae5..60d15cf51 100644 --- a/tests/test_core/test_auth.py +++ b/tests/test_core/test_auth.py @@ -11,6 +11,7 @@ from nose.tools import assert_raises from moto import mock_iam, mock_ec2, mock_s3, mock_sts, mock_elbv2, mock_rds2 from moto.core import set_initial_no_auth_action_count from moto.iam.models import ACCOUNT_ID +from uuid import uuid4 @mock_iam @@ -71,8 +72,10 @@ def create_user_with_access_key_and_multiple_policies( def create_group_with_attached_policy_and_add_user( - user_name, policy_document, group_name="test-group", policy_name="policy1" + user_name, policy_document, group_name="test-group", policy_name=None ): + if not policy_name: + policy_name = str(uuid4()) client = boto3.client("iam", region_name="us-east-1") client.create_group(GroupName=group_name) policy_arn = client.create_policy( @@ -101,8 +104,10 @@ def create_group_with_multiple_policies_and_add_user( attached_policy_document, group_name="test-group", inline_policy_name="policy1", - attached_policy_name="policy1", + attached_policy_name=None, ): + if not attached_policy_name: + attached_policy_name = str(uuid4()) client = boto3.client("iam", region_name="us-east-1") client.create_group(GroupName=group_name) client.put_group_policy( diff --git a/tests/test_ec2/test_vpcs.py b/tests/test_ec2/test_vpcs.py index 0894a8b8e..1bc3ddd98 100644 --- a/tests/test_ec2/test_vpcs.py +++ b/tests/test_ec2/test_vpcs.py @@ -678,3 +678,150 @@ def test_create_vpc_with_invalid_cidr_range(): "An error occurred (InvalidVpc.Range) when calling the CreateVpc " "operation: The CIDR '{}' is invalid.".format(vpc_cidr_block) ) + + +@mock_ec2 +def test_enable_vpc_classic_link(): + ec2 = boto3.resource("ec2", region_name="us-west-1") + + # Create VPC + vpc = ec2.create_vpc(CidrBlock="10.1.0.0/16") + + response = ec2.meta.client.enable_vpc_classic_link(VpcId=vpc.id) + assert response.get("Return").should.be.true + + +@mock_ec2 +def test_enable_vpc_classic_link_failure(): + ec2 = boto3.resource("ec2", region_name="us-west-1") + + # Create VPC + vpc = ec2.create_vpc(CidrBlock="10.90.0.0/16") + + response = ec2.meta.client.enable_vpc_classic_link(VpcId=vpc.id) + assert response.get("Return").should.be.false + + +@mock_ec2 +def test_disable_vpc_classic_link(): + ec2 = boto3.resource("ec2", region_name="us-west-1") + + # Create VPC + vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16") + + ec2.meta.client.enable_vpc_classic_link(VpcId=vpc.id) + response = ec2.meta.client.disable_vpc_classic_link(VpcId=vpc.id) + assert response.get("Return").should.be.false + + +@mock_ec2 +def test_describe_classic_link_enabled(): + ec2 = boto3.resource("ec2", region_name="us-west-1") + + # Create VPC + vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16") + + ec2.meta.client.enable_vpc_classic_link(VpcId=vpc.id) + response = ec2.meta.client.describe_vpc_classic_link(VpcIds=[vpc.id]) + assert response.get("Vpcs")[0].get("ClassicLinkEnabled").should.be.true + + +@mock_ec2 +def test_describe_classic_link_disabled(): + ec2 = boto3.resource("ec2", region_name="us-west-1") + + # Create VPC + vpc = ec2.create_vpc(CidrBlock="10.90.0.0/16") + + response = ec2.meta.client.describe_vpc_classic_link(VpcIds=[vpc.id]) + assert response.get("Vpcs")[0].get("ClassicLinkEnabled").should.be.false + + +@mock_ec2 +def test_describe_classic_link_multiple(): + ec2 = boto3.resource("ec2", region_name="us-west-1") + + # Create VPC + vpc1 = ec2.create_vpc(CidrBlock="10.90.0.0/16") + vpc2 = ec2.create_vpc(CidrBlock="10.0.0.0/16") + + ec2.meta.client.enable_vpc_classic_link(VpcId=vpc2.id) + response = ec2.meta.client.describe_vpc_classic_link(VpcIds=[vpc1.id, vpc2.id]) + expected = [ + {"VpcId": vpc1.id, "ClassicLinkDnsSupported": False}, + {"VpcId": vpc2.id, "ClassicLinkDnsSupported": True}, + ] + + # Ensure response is sorted, because they can come in random order + assert response.get("Vpcs").sort(key=lambda x: x["VpcId"]) == expected.sort( + key=lambda x: x["VpcId"] + ) + + +@mock_ec2 +def test_enable_vpc_classic_link_dns_support(): + ec2 = boto3.resource("ec2", region_name="us-west-1") + + # Create VPC + vpc = ec2.create_vpc(CidrBlock="10.1.0.0/16") + + response = ec2.meta.client.enable_vpc_classic_link_dns_support(VpcId=vpc.id) + assert response.get("Return").should.be.true + + +@mock_ec2 +def test_disable_vpc_classic_link_dns_support(): + ec2 = boto3.resource("ec2", region_name="us-west-1") + + # Create VPC + vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16") + + ec2.meta.client.enable_vpc_classic_link_dns_support(VpcId=vpc.id) + response = ec2.meta.client.disable_vpc_classic_link_dns_support(VpcId=vpc.id) + assert response.get("Return").should.be.false + + +@mock_ec2 +def test_describe_classic_link_dns_support_enabled(): + ec2 = boto3.resource("ec2", region_name="us-west-1") + + # Create VPC + vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16") + + ec2.meta.client.enable_vpc_classic_link_dns_support(VpcId=vpc.id) + response = ec2.meta.client.describe_vpc_classic_link_dns_support(VpcIds=[vpc.id]) + assert response.get("Vpcs")[0].get("ClassicLinkDnsSupported").should.be.true + + +@mock_ec2 +def test_describe_classic_link_dns_support_disabled(): + ec2 = boto3.resource("ec2", region_name="us-west-1") + + # Create VPC + vpc = ec2.create_vpc(CidrBlock="10.90.0.0/16") + + response = ec2.meta.client.describe_vpc_classic_link_dns_support(VpcIds=[vpc.id]) + assert response.get("Vpcs")[0].get("ClassicLinkDnsSupported").should.be.false + + +@mock_ec2 +def test_describe_classic_link_dns_support_multiple(): + ec2 = boto3.resource("ec2", region_name="us-west-1") + + # Create VPC + vpc1 = ec2.create_vpc(CidrBlock="10.90.0.0/16") + vpc2 = ec2.create_vpc(CidrBlock="10.0.0.0/16") + + ec2.meta.client.enable_vpc_classic_link_dns_support(VpcId=vpc2.id) + response = ec2.meta.client.describe_vpc_classic_link_dns_support( + VpcIds=[vpc1.id, vpc2.id] + ) + expected = [ + {"VpcId": vpc1.id, "ClassicLinkDnsSupported": False}, + {"VpcId": vpc2.id, "ClassicLinkDnsSupported": True}, + ] + + # Ensure response is sorted, because they can come in random order + assert response.get("Vpcs").sort(key=lambda x: x["VpcId"]) == expected.sort( + key=lambda x: x["VpcId"] + ) diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index c5e856b68..366ea3620 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -18,6 +18,7 @@ from nose.tools import raises from datetime import datetime from tests.helpers import requires_boto_gte +from uuid import uuid4 MOCK_CERT = """-----BEGIN CERTIFICATE----- @@ -1315,6 +1316,122 @@ def test_get_access_key_last_used(): resp["UserName"].should.equal(create_key_response["UserName"]) +@mock_iam +def test_upload_ssh_public_key(): + iam = boto3.resource("iam", region_name="us-east-1") + client = iam.meta.client + username = "test-user" + iam.create_user(UserName=username) + public_key = MOCK_CERT + + resp = client.upload_ssh_public_key(UserName=username, SSHPublicKeyBody=public_key) + pubkey = resp["SSHPublicKey"] + pubkey["SSHPublicKeyBody"].should.equal(public_key) + pubkey["UserName"].should.equal(username) + pubkey["SSHPublicKeyId"].should.have.length_of(20) + assert pubkey["SSHPublicKeyId"].startswith("APKA") + pubkey.should.have.key("Fingerprint") + pubkey["Status"].should.equal("Active") + ( + datetime.utcnow() - pubkey["UploadDate"].replace(tzinfo=None) + ).seconds.should.be.within(0, 10) + + +@mock_iam +def test_get_ssh_public_key(): + iam = boto3.resource("iam", region_name="us-east-1") + client = iam.meta.client + username = "test-user" + iam.create_user(UserName=username) + public_key = MOCK_CERT + + with assert_raises(ClientError): + client.get_ssh_public_key( + UserName=username, SSHPublicKeyId="xxnon-existent-keyxx", Encoding="SSH" + ) + + resp = client.upload_ssh_public_key(UserName=username, SSHPublicKeyBody=public_key) + ssh_public_key_id = resp["SSHPublicKey"]["SSHPublicKeyId"] + + resp = client.get_ssh_public_key( + UserName=username, SSHPublicKeyId=ssh_public_key_id, Encoding="SSH" + ) + resp["SSHPublicKey"]["SSHPublicKeyBody"].should.equal(public_key) + + +@mock_iam +def test_list_ssh_public_keys(): + iam = boto3.resource("iam", region_name="us-east-1") + client = iam.meta.client + username = "test-user" + iam.create_user(UserName=username) + public_key = MOCK_CERT + + resp = client.list_ssh_public_keys(UserName=username) + resp["SSHPublicKeys"].should.have.length_of(0) + + resp = client.upload_ssh_public_key(UserName=username, SSHPublicKeyBody=public_key) + ssh_public_key_id = resp["SSHPublicKey"]["SSHPublicKeyId"] + + resp = client.list_ssh_public_keys(UserName=username) + resp["SSHPublicKeys"].should.have.length_of(1) + resp["SSHPublicKeys"][0]["SSHPublicKeyId"].should.equal(ssh_public_key_id) + + +@mock_iam +def test_update_ssh_public_key(): + iam = boto3.resource("iam", region_name="us-east-1") + client = iam.meta.client + username = "test-user" + iam.create_user(UserName=username) + public_key = MOCK_CERT + + with assert_raises(ClientError): + client.update_ssh_public_key( + UserName=username, SSHPublicKeyId="xxnon-existent-keyxx", Status="Inactive" + ) + + resp = client.upload_ssh_public_key(UserName=username, SSHPublicKeyBody=public_key) + ssh_public_key_id = resp["SSHPublicKey"]["SSHPublicKeyId"] + resp["SSHPublicKey"]["Status"].should.equal("Active") + + resp = client.update_ssh_public_key( + UserName=username, SSHPublicKeyId=ssh_public_key_id, Status="Inactive" + ) + + resp = client.get_ssh_public_key( + UserName=username, SSHPublicKeyId=ssh_public_key_id, Encoding="SSH" + ) + resp["SSHPublicKey"]["Status"].should.equal("Inactive") + + +@mock_iam +def test_delete_ssh_public_key(): + iam = boto3.resource("iam", region_name="us-east-1") + client = iam.meta.client + username = "test-user" + iam.create_user(UserName=username) + public_key = MOCK_CERT + + with assert_raises(ClientError): + client.delete_ssh_public_key( + UserName=username, SSHPublicKeyId="xxnon-existent-keyxx" + ) + + resp = client.upload_ssh_public_key(UserName=username, SSHPublicKeyBody=public_key) + ssh_public_key_id = resp["SSHPublicKey"]["SSHPublicKeyId"] + + resp = client.list_ssh_public_keys(UserName=username) + resp["SSHPublicKeys"].should.have.length_of(1) + + resp = client.delete_ssh_public_key( + UserName=username, SSHPublicKeyId=ssh_public_key_id + ) + + resp = client.list_ssh_public_keys(UserName=username) + resp["SSHPublicKeys"].should.have.length_of(0) + + @mock_iam def test_get_account_authorization_details(): test_policy = json.dumps( @@ -2050,6 +2167,42 @@ def test_create_role_with_permissions_boundary(): conn.list_roles().get("Roles")[0].get("PermissionsBoundary").should.equal(expected) +@mock_iam +def test_create_role_with_same_name_should_fail(): + iam = boto3.client("iam", region_name="us-east-1") + test_role_name = str(uuid4()) + iam.create_role( + RoleName=test_role_name, AssumeRolePolicyDocument="policy", Description="test" + ) + # Create the role again, and verify that it fails + with assert_raises(ClientError) as err: + iam.create_role( + RoleName=test_role_name, + AssumeRolePolicyDocument="policy", + Description="test", + ) + err.exception.response["Error"]["Code"].should.equal("EntityAlreadyExists") + err.exception.response["Error"]["Message"].should.equal( + "Role with name {0} already exists.".format(test_role_name) + ) + + +@mock_iam +def test_create_policy_with_same_name_should_fail(): + iam = boto3.client("iam", region_name="us-east-1") + test_policy_name = str(uuid4()) + policy = iam.create_policy(PolicyName=test_policy_name, PolicyDocument=MOCK_POLICY) + # Create the role again, and verify that it fails + with assert_raises(ClientError) as err: + iam.create_policy(PolicyName=test_policy_name, PolicyDocument=MOCK_POLICY) + err.exception.response["Error"]["Code"].should.equal("EntityAlreadyExists") + err.exception.response["Error"]["Message"].should.equal( + "A policy called {0} already exists. Duplicate names are not allowed.".format( + test_policy_name + ) + ) + + @mock_iam def test_create_open_id_connect_provider(): client = boto3.client("iam", region_name="us-east-1") @@ -2325,3 +2478,123 @@ def test_delete_account_password_policy_errors(): client.delete_account_password_policy.when.called_with().should.throw( ClientError, "The account policy with name PasswordPolicy cannot be found." ) + + +@mock_iam +def test_get_account_summary(): + client = boto3.client("iam", region_name="us-east-1") + iam = boto3.resource("iam", region_name="us-east-1") + + account_summary = iam.AccountSummary() + + account_summary.summary_map.should.equal( + { + "GroupPolicySizeQuota": 5120, + "InstanceProfilesQuota": 1000, + "Policies": 0, + "GroupsPerUserQuota": 10, + "InstanceProfiles": 0, + "AttachedPoliciesPerUserQuota": 10, + "Users": 0, + "PoliciesQuota": 1500, + "Providers": 0, + "AccountMFAEnabled": 0, + "AccessKeysPerUserQuota": 2, + "AssumeRolePolicySizeQuota": 2048, + "PolicyVersionsInUseQuota": 10000, + "GlobalEndpointTokenVersion": 1, + "VersionsPerPolicyQuota": 5, + "AttachedPoliciesPerGroupQuota": 10, + "PolicySizeQuota": 6144, + "Groups": 0, + "AccountSigningCertificatesPresent": 0, + "UsersQuota": 5000, + "ServerCertificatesQuota": 20, + "MFADevices": 0, + "UserPolicySizeQuota": 2048, + "PolicyVersionsInUse": 0, + "ServerCertificates": 0, + "Roles": 0, + "RolesQuota": 1000, + "SigningCertificatesPerUserQuota": 2, + "MFADevicesInUse": 0, + "RolePolicySizeQuota": 10240, + "AttachedPoliciesPerRoleQuota": 10, + "AccountAccessKeysPresent": 0, + "GroupsQuota": 300, + } + ) + + client.create_instance_profile(InstanceProfileName="test-profile") + client.create_open_id_connect_provider( + Url="https://example.com", ThumbprintList=[], + ) + response_policy = client.create_policy( + PolicyName="test-policy", PolicyDocument=MOCK_POLICY + ) + client.create_role(RoleName="test-role", AssumeRolePolicyDocument="test policy") + client.attach_role_policy( + RoleName="test-role", PolicyArn=response_policy["Policy"]["Arn"] + ) + client.create_saml_provider( + Name="TestSAMLProvider", SAMLMetadataDocument="a" * 1024 + ) + client.create_group(GroupName="test-group") + client.attach_group_policy( + GroupName="test-group", PolicyArn=response_policy["Policy"]["Arn"] + ) + client.create_user(UserName="test-user") + client.attach_user_policy( + UserName="test-user", PolicyArn=response_policy["Policy"]["Arn"] + ) + client.enable_mfa_device( + UserName="test-user", + SerialNumber="123456789", + AuthenticationCode1="234567", + AuthenticationCode2="987654", + ) + client.create_virtual_mfa_device(VirtualMFADeviceName="test-device") + client.upload_server_certificate( + ServerCertificateName="test-cert", + CertificateBody="cert-body", + PrivateKey="private-key", + ) + account_summary.load() + + account_summary.summary_map.should.equal( + { + "GroupPolicySizeQuota": 5120, + "InstanceProfilesQuota": 1000, + "Policies": 1, + "GroupsPerUserQuota": 10, + "InstanceProfiles": 1, + "AttachedPoliciesPerUserQuota": 10, + "Users": 1, + "PoliciesQuota": 1500, + "Providers": 2, + "AccountMFAEnabled": 0, + "AccessKeysPerUserQuota": 2, + "AssumeRolePolicySizeQuota": 2048, + "PolicyVersionsInUseQuota": 10000, + "GlobalEndpointTokenVersion": 1, + "VersionsPerPolicyQuota": 5, + "AttachedPoliciesPerGroupQuota": 10, + "PolicySizeQuota": 6144, + "Groups": 1, + "AccountSigningCertificatesPresent": 0, + "UsersQuota": 5000, + "ServerCertificatesQuota": 20, + "MFADevices": 1, + "UserPolicySizeQuota": 2048, + "PolicyVersionsInUse": 3, + "ServerCertificates": 1, + "Roles": 1, + "RolesQuota": 1000, + "SigningCertificatesPerUserQuota": 2, + "MFADevicesInUse": 1, + "RolePolicySizeQuota": 10240, + "AttachedPoliciesPerRoleQuota": 10, + "AccountAccessKeysPresent": 0, + "GroupsQuota": 300, + } + ) diff --git a/tests/test_sns/test_publishing_boto3.py b/tests/test_sns/test_publishing_boto3.py index 64669d5e0..5bda0720c 100644 --- a/tests/test_sns/test_publishing_boto3.py +++ b/tests/test_sns/test_publishing_boto3.py @@ -173,6 +173,27 @@ def test_publish_to_sqs_msg_attr_byte_value(): ) +@mock_sqs +@mock_sns +def test_publish_to_sqs_msg_attr_number_type(): + sns = boto3.resource("sns", region_name="us-east-1") + topic = sns.create_topic(Name="test-topic") + sqs = boto3.resource("sqs", region_name="us-east-1") + queue = sqs.create_queue(QueueName="test-queue") + topic.subscribe(Protocol="sqs", Endpoint=queue.attributes["QueueArn"]) + + topic.publish( + Message="test message", + MessageAttributes={"retries": {"DataType": "Number", "StringValue": "0"}}, + ) + + message = json.loads(queue.receive_messages()[0].body) + message["Message"].should.equal("test message") + message["MessageAttributes"].should.equal( + {"retries": {"Type": "Number", "Value": 0}} + ) + + @mock_sns def test_publish_sms(): client = boto3.client("sns", region_name="us-east-1")