diff --git a/.travis.yml b/.travis.yml index f1854db54..ac9322211 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -dist: xenial +dist: bionic language: python services: - docker @@ -54,7 +54,7 @@ deploy: on: branch: - master - cleanup: false + skip_cleanup: true skip_existing: true # - provider: pypi # distributions: sdist bdist_wheel diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index f8ccf66d4..cd1d4d482 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 @@ -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/core/models.py b/moto/core/models.py index e0eae5858..5b19137c3 100644 --- a/moto/core/models.py +++ b/moto/core/models.py @@ -44,6 +44,7 @@ class BaseMockAWS(object): "AWS_ACCESS_KEY_ID": "foobar_key", "AWS_SECRET_ACCESS_KEY": "foobar_secret", } + self.default_session_mock = mock.patch("boto3.DEFAULT_SESSION", None) self.env_variables_mocks = mock.patch.dict(os.environ, FAKE_KEYS) if self.__class__.nested_count == 0: @@ -62,6 +63,7 @@ class BaseMockAWS(object): self.stop() def start(self, reset=True): + self.default_session_mock.start() self.env_variables_mocks.start() self.__class__.nested_count += 1 @@ -72,6 +74,7 @@ class BaseMockAWS(object): self.enable_patching() def stop(self): + self.default_session_mock.stop() self.env_variables_mocks.stop() self.__class__.nested_count -= 1 diff --git a/moto/core/responses.py b/moto/core/responses.py index bf4af902a..c708edb8b 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -307,7 +307,7 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin): def _convert(elem, is_last): if not re.match("^{.*}$", elem): return elem - name = elem.replace("{", "").replace("}", "") + name = elem.replace("{", "").replace("}", "").replace("+", "") if is_last: return "(?P<%s>[^/]*)" % name return "(?P<%s>.*)" % name diff --git a/moto/datasync/responses.py b/moto/datasync/responses.py index 23a480523..03811fb6e 100644 --- a/moto/datasync/responses.py +++ b/moto/datasync/responses.py @@ -153,7 +153,7 @@ class DataSyncResponse(BaseResponse): task_execution_arn = self._get_param("TaskExecutionArn") task_execution = self.datasync_backend._get_task_execution(task_execution_arn) result = json.dumps( - {"TaskExecutionArn": task_execution.arn, "Status": task_execution.status,} + {"TaskExecutionArn": task_execution.arn, "Status": task_execution.status} ) if task_execution.status == "SUCCESS": self.datasync_backend.tasks[task_execution.task_arn].status = "AVAILABLE" diff --git a/moto/datasync/urls.py b/moto/datasync/urls.py index b70a09f27..69ba3cccb 100644 --- a/moto/datasync/urls.py +++ b/moto/datasync/urls.py @@ -4,6 +4,4 @@ from .responses import DataSyncResponse url_bases = ["https?://(.*?)(datasync)(.*?).amazonaws.com"] -url_paths = { - "{0}/$": DataSyncResponse.dispatch, -} +url_paths = {"{0}/$": DataSyncResponse.dispatch} diff --git a/moto/events/models.py b/moto/events/models.py index be4153b9f..0298c7c69 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -316,8 +316,7 @@ class EventsBackend(BaseBackend): if not event_bus: raise JsonRESTError( - "ResourceNotFoundException", - "Event bus {} does not exist.".format(name), + "ResourceNotFoundException", "Event bus {} does not exist.".format(name) ) return event_bus diff --git a/moto/events/responses.py b/moto/events/responses.py index 98a33218a..b415564f8 100644 --- a/moto/events/responses.py +++ b/moto/events/responses.py @@ -261,10 +261,7 @@ class EventsHandler(BaseResponse): name = self._get_param("Name") event_bus = self.events_backend.describe_event_bus(name) - response = { - "Name": event_bus.name, - "Arn": event_bus.arn, - } + response = {"Name": event_bus.name, "Arn": event_bus.arn} if event_bus.policy: response["Policy"] = event_bus.policy @@ -285,10 +282,7 @@ class EventsHandler(BaseResponse): response = [] for event_bus in self.events_backend.list_event_buses(name_prefix): - event_bus_response = { - "Name": event_bus.name, - "Arn": event_bus.arn, - } + event_bus_response = {"Name": event_bus.name, "Arn": event_bus.arn} if event_bus.policy: event_bus_response["Policy"] = event_bus.policy diff --git a/moto/iam/models.py b/moto/iam/models.py index df4fa987c..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): @@ -1162,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() @@ -1455,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 ): @@ -1741,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/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/tests/test_apigateway/test_apigateway.py b/tests/test_apigateway/test_apigateway.py index de99f1fcf..8da5efa39 100644 --- a/tests/test_apigateway/test_apigateway.py +++ b/tests/test_apigateway/test_apigateway.py @@ -563,7 +563,7 @@ def test_create_stage(): api_id = response["id"] create_method_integration(client, api_id) - response = client.create_deployment(restApiId=api_id, stageName=stage_name,) + response = client.create_deployment(restApiId=api_id, stageName=stage_name) deployment_id = response["id"] response = client.get_deployment(restApiId=api_id, deploymentId=deployment_id) diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py index 306deeea4..5d66b1537 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) @@ -999,10 +1001,10 @@ def test_list_versions_by_function_for_nonexistent_function(): @mock_lambda @mock_sqs def test_create_event_source_mapping(): - sqs = boto3.resource("sqs") + sqs = boto3.resource("sqs", region_name="us-east-1") queue = sqs.create_queue(QueueName="test-sqs-queue1") - conn = boto3.client("lambda") + conn = boto3.client("lambda", region_name="us-east-1") func = conn.create_function( FunctionName="testFunction", Runtime="python2.7", @@ -1028,11 +1030,11 @@ def test_create_event_source_mapping(): @mock_lambda @mock_sqs def test_invoke_function_from_sqs(): - logs_conn = boto3.client("logs") - sqs = boto3.resource("sqs") + logs_conn = boto3.client("logs", region_name="us-east-1") + sqs = boto3.resource("sqs", region_name="us-east-1") queue = sqs.create_queue(QueueName="test-sqs-queue1") - conn = boto3.client("lambda") + conn = boto3.client("lambda", region_name="us-east-1") func = conn.create_function( FunctionName="testFunction", Runtime="python2.7", @@ -1052,7 +1054,7 @@ def test_invoke_function_from_sqs(): assert response["EventSourceArn"] == queue.attributes["QueueArn"] assert response["State"] == "Enabled" - sqs_client = boto3.client("sqs") + sqs_client = boto3.client("sqs", region_name="us-east-1") sqs_client.send_message(QueueUrl=queue.url, MessageBody="test") start = time.time() while (time.time() - start) < 30: @@ -1079,8 +1081,8 @@ def test_invoke_function_from_sqs(): @mock_lambda @mock_dynamodb2 def test_invoke_function_from_dynamodb(): - logs_conn = boto3.client("logs") - dynamodb = boto3.client("dynamodb") + logs_conn = boto3.client("logs", region_name="us-east-1") + dynamodb = boto3.client("dynamodb", region_name="us-east-1") table_name = "table_with_stream" table = dynamodb.create_table( TableName=table_name, @@ -1092,7 +1094,7 @@ def test_invoke_function_from_dynamodb(): }, ) - conn = boto3.client("lambda") + conn = boto3.client("lambda", region_name="us-east-1") func = conn.create_function( FunctionName="testFunction", Runtime="python2.7", @@ -1139,11 +1141,11 @@ def test_invoke_function_from_dynamodb(): @mock_lambda @mock_sqs def test_invoke_function_from_sqs_exception(): - logs_conn = boto3.client("logs") - sqs = boto3.resource("sqs") + logs_conn = boto3.client("logs", region_name="us-east-1") + sqs = boto3.resource("sqs", region_name="us-east-1") queue = sqs.create_queue(QueueName="test-sqs-queue1") - conn = boto3.client("lambda") + conn = boto3.client("lambda", region_name="us-east-1") func = conn.create_function( FunctionName="testFunction", Runtime="python2.7", @@ -1199,10 +1201,10 @@ def test_invoke_function_from_sqs_exception(): @mock_lambda @mock_sqs def test_list_event_source_mappings(): - sqs = boto3.resource("sqs") + sqs = boto3.resource("sqs", region_name="us-east-1") queue = sqs.create_queue(QueueName="test-sqs-queue1") - conn = boto3.client("lambda") + conn = boto3.client("lambda", region_name="us-east-1") func = conn.create_function( FunctionName="testFunction", Runtime="python2.7", @@ -1231,10 +1233,10 @@ def test_list_event_source_mappings(): @mock_lambda @mock_sqs def test_get_event_source_mapping(): - sqs = boto3.resource("sqs") + sqs = boto3.resource("sqs", region_name="us-east-1") queue = sqs.create_queue(QueueName="test-sqs-queue1") - conn = boto3.client("lambda") + conn = boto3.client("lambda", region_name="us-east-1") func = conn.create_function( FunctionName="testFunction", Runtime="python2.7", @@ -1261,10 +1263,10 @@ def test_get_event_source_mapping(): @mock_lambda @mock_sqs def test_update_event_source_mapping(): - sqs = boto3.resource("sqs") + sqs = boto3.resource("sqs", region_name="us-east-1") queue = sqs.create_queue(QueueName="test-sqs-queue1") - conn = boto3.client("lambda") + conn = boto3.client("lambda", region_name="us-east-1") func1 = conn.create_function( FunctionName="testFunction", Runtime="python2.7", @@ -1305,10 +1307,10 @@ def test_update_event_source_mapping(): @mock_lambda @mock_sqs def test_delete_event_source_mapping(): - sqs = boto3.resource("sqs") + sqs = boto3.resource("sqs", region_name="us-east-1") queue = sqs.create_queue(QueueName="test-sqs-queue1") - conn = boto3.client("lambda") + conn = boto3.client("lambda", region_name="us-east-1") func1 = conn.create_function( FunctionName="testFunction", Runtime="python2.7", diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py index 7f6963870..456311028 100644 --- a/tests/test_dynamodb2/test_dynamodb.py +++ b/tests/test_dynamodb2/test_dynamodb.py @@ -3119,8 +3119,8 @@ def test_sorted_query_with_numerical_sort_key(): # https://github.com/spulec/moto/issues/1874 @mock_dynamodb2 def test_item_size_is_under_400KB(): - dynamodb = boto3.resource("dynamodb") - client = boto3.client("dynamodb") + dynamodb = boto3.resource("dynamodb", region_name="us-east-1") + client = boto3.client("dynamodb", region_name="us-east-1") dynamodb.create_table( TableName="moto-test", @@ -3172,7 +3172,7 @@ def assert_failure_due_to_item_size(func, **kwargs): @mock_dynamodb2 # https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html#DDB-Query-request-KeyConditionExpression def test_hash_key_cannot_use_begins_with_operations(): - dynamodb = boto3.resource("dynamodb") + dynamodb = boto3.resource("dynamodb", region_name="us-east-1") table = dynamodb.create_table( TableName="test-table", KeySchema=[{"AttributeName": "key", "KeyType": "HASH"}], @@ -3201,7 +3201,7 @@ def test_hash_key_cannot_use_begins_with_operations(): @mock_dynamodb2 def test_update_supports_complex_expression_attribute_values(): - client = boto3.client("dynamodb") + client = boto3.client("dynamodb", region_name="us-east-1") client.create_table( AttributeDefinitions=[{"AttributeName": "SHA256", "AttributeType": "S"}], @@ -3237,7 +3237,7 @@ def test_update_supports_complex_expression_attribute_values(): @mock_dynamodb2 def test_update_supports_list_append(): - client = boto3.client("dynamodb") + client = boto3.client("dynamodb", region_name="us-east-1") client.create_table( AttributeDefinitions=[{"AttributeName": "SHA256", "AttributeType": "S"}], @@ -3272,7 +3272,7 @@ def test_update_supports_list_append(): @mock_dynamodb2 def test_update_catches_invalid_list_append_operation(): - client = boto3.client("dynamodb") + client = boto3.client("dynamodb", region_name="us-east-1") client.create_table( AttributeDefinitions=[{"AttributeName": "SHA256", "AttributeType": "S"}], @@ -3335,7 +3335,7 @@ def test_update_item_if_original_value_is_none(): table.update_item( Key={"job_id": "a"}, UpdateExpression="SET job_name = :output", - ExpressionAttributeValues={":output": "updated",}, + ExpressionAttributeValues={":output": "updated"}, ) table.scan()["Items"][0]["job_name"].should.equal("updated") @@ -3354,7 +3354,7 @@ def test_update_nested_item_if_original_value_is_none(): table.update_item( Key={"job_id": "a"}, UpdateExpression="SET job_details.job_name = :output", - ExpressionAttributeValues={":output": "updated",}, + ExpressionAttributeValues={":output": "updated"}, ) table.scan()["Items"][0]["job_details"]["job_name"].should.equal("updated") diff --git a/tests/test_dynamodbstreams/test_dynamodbstreams.py b/tests/test_dynamodbstreams/test_dynamodbstreams.py index a98f97bf3..8fad0ff23 100644 --- a/tests/test_dynamodbstreams/test_dynamodbstreams.py +++ b/tests/test_dynamodbstreams/test_dynamodbstreams.py @@ -213,7 +213,7 @@ class TestEdges: resp = conn.update_table( TableName="test-streams", - StreamSpecification={"StreamEnabled": True, "StreamViewType": "KEYS_ONLY"}, + StreamSpecification={"StreamViewType": "KEYS_ONLY", "StreamEnabled": True}, ) assert "StreamSpecification" in resp["TableDescription"] assert resp["TableDescription"]["StreamSpecification"] == { @@ -227,8 +227,8 @@ class TestEdges: resp = conn.update_table( TableName="test-streams", StreamSpecification={ - "StreamEnabled": True, "StreamViewType": "OLD_IMAGES", + "StreamEnabled": True, }, ) @@ -246,7 +246,7 @@ class TestEdges: {"AttributeName": "color", "AttributeType": "S"}, ], ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, - StreamSpecification={"StreamEnabled": True, "StreamViewType": "NEW_IMAGES"}, + StreamSpecification={"StreamViewType": "NEW_IMAGES", "StreamEnabled": True}, ) stream_arn = resp["TableDescription"]["LatestStreamArn"] diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index 61b0d534c..5098de413 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -1316,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( @@ -2362,3 +2478,121 @@ 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_stepfunctions/test_stepfunctions.py b/tests/test_stepfunctions/test_stepfunctions.py index 77b9fbfb3..6c391f0d1 100644 --- a/tests/test_stepfunctions/test_stepfunctions.py +++ b/tests/test_stepfunctions/test_stepfunctions.py @@ -524,7 +524,7 @@ def _get_account_id(): global account_id if account_id: return account_id - sts = boto3.client("sts") + sts = boto3.client("sts", region_name=region) identity = sts.get_caller_identity() account_id = identity["Account"] return account_id