From efb19b92f0c0843b70684bc22e03ebaa992386ef Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Sat, 19 Feb 2022 23:45:36 -0100 Subject: [PATCH] CloudTrail improvements (#4875) --- IMPLEMENTATION_COVERAGE.md | 18 +- docs/docs/services/cloudtrail.rst | 20 +- moto/cloudtrail/models.py | 161 +++++++++++++- moto/cloudtrail/responses.py | 119 +++++++++- moto/organizations/models.py | 9 +- tests/terraform-tests.success.txt | 1 + tests/test_cloudtrail/test_cloudtrail.py | 76 ++++++- .../test_cloudtrail_eventselectors.py | 208 ++++++++++++++++++ tests/test_cloudtrail/test_cloudtrail_tags.py | 77 +++++++ .../organizations_test_utils.py | 6 +- .../test_organizations_boto3.py | 14 +- 11 files changed, 665 insertions(+), 44 deletions(-) create mode 100644 tests/test_cloudtrail/test_cloudtrail_eventselectors.py create mode 100644 tests/test_cloudtrail/test_cloudtrail_tags.py diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index f9088b4ec..b4e062acb 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -632,9 +632,9 @@ ## cloudtrail
-27% implemented +55% implemented -- [ ] add_tags +- [X] add_tags - [ ] cancel_query - [ ] create_event_data_store - [X] create_trail @@ -643,26 +643,26 @@ - [ ] describe_query - [X] describe_trails - [ ] get_event_data_store -- [ ] get_event_selectors -- [ ] get_insight_selectors +- [X] get_event_selectors +- [X] get_insight_selectors - [ ] get_query_results - [X] get_trail - [X] get_trail_status - [ ] list_event_data_stores - [ ] list_public_keys - [ ] list_queries -- [ ] list_tags +- [X] list_tags - [X] list_trails - [ ] lookup_events -- [ ] put_event_selectors -- [ ] put_insight_selectors -- [ ] remove_tags +- [X] put_event_selectors +- [X] put_insight_selectors +- [X] remove_tags - [ ] restore_event_data_store - [X] start_logging - [ ] start_query - [X] stop_logging - [ ] update_event_data_store -- [ ] update_trail +- [X] update_trail
## cloudwatch diff --git a/docs/docs/services/cloudtrail.rst b/docs/docs/services/cloudtrail.rst index d14f6cc83..0650c11d0 100644 --- a/docs/docs/services/cloudtrail.rst +++ b/docs/docs/services/cloudtrail.rst @@ -27,7 +27,7 @@ cloudtrail |start-h3| Implemented features for this service |end-h3| -- [ ] add_tags +- [X] add_tags - [ ] cancel_query - [ ] create_event_data_store - [X] create_trail @@ -36,24 +36,28 @@ cloudtrail - [ ] describe_query - [X] describe_trails - [ ] get_event_data_store -- [ ] get_event_selectors -- [ ] get_insight_selectors +- [X] get_event_selectors +- [X] get_insight_selectors - [ ] get_query_results - [X] get_trail - [X] get_trail_status - [ ] list_event_data_stores - [ ] list_public_keys - [ ] list_queries -- [ ] list_tags +- [X] list_tags + + Pagination is not yet implemented + + - [X] list_trails - [ ] lookup_events -- [ ] put_event_selectors -- [ ] put_insight_selectors -- [ ] remove_tags +- [X] put_event_selectors +- [X] put_insight_selectors +- [X] remove_tags - [ ] restore_event_data_store - [X] start_logging - [ ] start_query - [X] stop_logging - [ ] update_event_data_store -- [ ] update_trail +- [X] update_trail diff --git a/moto/cloudtrail/models.py b/moto/cloudtrail/models.py index c84052b2c..e8e731b86 100644 --- a/moto/cloudtrail/models.py +++ b/moto/cloudtrail/models.py @@ -4,6 +4,7 @@ import time from datetime import datetime from moto.core import ACCOUNT_ID, BaseBackend, BaseModel from moto.core.utils import iso_8601_datetime_without_milliseconds, BackendDict +from moto.utilities.tagging_service import TaggingService from .exceptions import ( S3BucketDoesNotExistException, InsufficientSnsTopicPolicyException, @@ -78,9 +79,13 @@ class Trail(BaseModel): bucket_name, s3_key_prefix, sns_topic_name, + is_global, is_multi_region, log_validation, is_org_trail, + cw_log_group_arn, + cw_role_arn, + kms_key_id, ): self.region_name = region_name self.trail_name = trail_name @@ -90,10 +95,17 @@ class Trail(BaseModel): self.is_multi_region = is_multi_region self.log_validation = log_validation self.is_org_trail = is_org_trail + self.include_global_service_events = is_global + self.cw_log_group_arn = cw_log_group_arn + self.cw_role_arn = cw_role_arn + self.kms_key_id = kms_key_id self.check_name() self.check_bucket_exists() self.check_topic_exists() self.status = TrailStatus() + self.event_selectors = list() + self.advanced_event_selectors = list() + self.insight_selectors = list() @property def arn(self): @@ -145,6 +157,56 @@ class Trail(BaseModel): def stop_logging(self): self.status.stop_logging() + def put_event_selectors(self, event_selectors, advanced_event_selectors): + if event_selectors: + self.event_selectors = event_selectors + elif advanced_event_selectors: + self.event_selectors = [] + self.advanced_event_selectors = advanced_event_selectors + + def get_event_selectors(self): + return self.event_selectors, self.advanced_event_selectors + + def put_insight_selectors(self, insight_selectors): + self.insight_selectors.extend(insight_selectors) + + def get_insight_selectors(self): + return self.insight_selectors + + def update( + self, + s3_bucket_name, + s3_key_prefix, + sns_topic_name, + include_global_service_events, + is_multi_region_trail, + enable_log_file_validation, + is_organization_trail, + cw_log_group_arn, + cw_role_arn, + kms_key_id, + ): + if s3_bucket_name is not None: + self.bucket_name = s3_bucket_name + if s3_key_prefix is not None: + self.s3_key_prefix = s3_key_prefix + if sns_topic_name is not None: + self.sns_topic_name = sns_topic_name + if include_global_service_events is not None: + self.include_global_service_events = include_global_service_events + if is_multi_region_trail is not None: + self.is_multi_region = is_multi_region_trail + if enable_log_file_validation is not None: + self.log_validation = enable_log_file_validation + if is_organization_trail is not None: + self.is_org_trail = is_organization_trail + if cw_log_group_arn is not None: + self.cw_log_group_arn = cw_log_group_arn + if cw_role_arn is not None: + self.cw_role_arn = cw_role_arn + if kms_key_id is not None: + self.kms_key_id = kms_key_id + def short(self): return { "Name": self.trail_name, @@ -156,13 +218,16 @@ class Trail(BaseModel): desc = { "Name": self.trail_name, "S3BucketName": self.bucket_name, - "IncludeGlobalServiceEvents": True, + "IncludeGlobalServiceEvents": self.include_global_service_events, "IsMultiRegionTrail": self.is_multi_region, "TrailARN": self.arn, "LogFileValidationEnabled": self.log_validation, "IsOrganizationTrail": self.is_org_trail, "HasCustomEventSelectors": False, "HasInsightSelectors": False, + "CloudWatchLogsLogGroupArn": self.cw_log_group_arn, + "CloudWatchLogsRoleArn": self.cw_role_arn, + "KmsKeyId": self.kms_key_id, } if self.s3_key_prefix is not None: desc["S3KeyPrefix"] = self.s3_key_prefix @@ -180,6 +245,7 @@ class CloudTrailBackend(BaseBackend): def __init__(self, region_name): self.region_name = region_name self.trails = dict() + self.tagging_service = TaggingService(tag_name="TagsList") def create_trail( self, @@ -187,9 +253,14 @@ class CloudTrailBackend(BaseBackend): bucket_name, s3_key_prefix, sns_topic_name, + is_global, is_multi_region, log_validation, is_org_trail, + cw_log_group_arn, + cw_role_arn, + kms_key_id, + tags_list, ): trail = Trail( self.region_name, @@ -197,19 +268,27 @@ class CloudTrailBackend(BaseBackend): bucket_name, s3_key_prefix, sns_topic_name, + is_global, is_multi_region, log_validation, is_org_trail, + cw_log_group_arn, + cw_role_arn, + kms_key_id, ) self.trails[name] = trail + self.tagging_service.tag_resource(trail.arn, tags_list) return trail - def get_trail(self, name): - if len(name) < 3: - raise TrailNameTooShort(actual_length=len(name)) - if name not in self.trails: - raise TrailNotFoundException(name) - return self.trails[name] + def get_trail(self, name_or_arn): + if len(name_or_arn) < 3: + raise TrailNameTooShort(actual_length=len(name_or_arn)) + if name_or_arn in self.trails: + return self.trails[name_or_arn] + for trail in self.trails.values(): + if trail.arn == name_or_arn: + return trail + raise TrailNotFoundException(name_or_arn) def get_trail_status(self, name): if len(name) < 3: @@ -253,11 +332,79 @@ class CloudTrailBackend(BaseBackend): if name in self.trails: del self.trails[name] + def update_trail( + self, + name, + s3_bucket_name, + s3_key_prefix, + sns_topic_name, + include_global_service_events, + is_multi_region_trail, + enable_log_file_validation, + is_organization_trail, + cw_log_group_arn, + cw_role_arn, + kms_key_id, + ): + trail = self.get_trail(name_or_arn=name) + trail.update( + s3_bucket_name=s3_bucket_name, + s3_key_prefix=s3_key_prefix, + sns_topic_name=sns_topic_name, + include_global_service_events=include_global_service_events, + is_multi_region_trail=is_multi_region_trail, + enable_log_file_validation=enable_log_file_validation, + is_organization_trail=is_organization_trail, + cw_log_group_arn=cw_log_group_arn, + cw_role_arn=cw_role_arn, + kms_key_id=kms_key_id, + ) + return trail + def reset(self): """Re-initialize all attributes for this instance.""" region_name = self.region_name self.__dict__ = {} self.__init__(region_name) + def put_event_selectors( + self, trail_name, event_selectors, advanced_event_selectors + ): + trail = self.get_trail(trail_name) + trail.put_event_selectors(event_selectors, advanced_event_selectors) + trail_arn = trail.arn + return trail_arn, event_selectors, advanced_event_selectors + + def get_event_selectors(self, trail_name): + trail = self.get_trail(trail_name) + event_selectors, advanced_event_selectors = trail.get_event_selectors() + return trail.arn, event_selectors, advanced_event_selectors + + def add_tags(self, resource_id, tags_list): + self.tagging_service.tag_resource(resource_id, tags_list) + + def remove_tags(self, resource_id, tags_list): + self.tagging_service.untag_resource_using_tags(resource_id, tags_list) + + def list_tags(self, resource_id_list): + """ + Pagination is not yet implemented + """ + resp = [{"ResourceId": r_id} for r_id in resource_id_list] + for item in resp: + item["TagsList"] = self.tagging_service.list_tags_for_resource( + item["ResourceId"] + )["TagsList"] + return resp + + def put_insight_selectors(self, trail_name, insight_selectors): + trail = self.get_trail(trail_name) + trail.put_insight_selectors(insight_selectors) + return trail.arn, insight_selectors + + def get_insight_selectors(self, trail_name): + trail = self.get_trail(trail_name) + return trail.arn, trail.get_insight_selectors() + cloudtrail_backends = BackendDict(CloudTrailBackend, "cloudtrail") diff --git a/moto/cloudtrail/responses.py b/moto/cloudtrail/responses.py index 9fa79ee64..6d1aa452d 100644 --- a/moto/cloudtrail/responses.py +++ b/moto/cloudtrail/responses.py @@ -17,7 +17,7 @@ class CloudTrailResponse(BaseResponse): def create_trail(self): name = self._get_param("Name") bucket_name = self._get_param("S3BucketName") - is_global = self._get_bool_param("IncludeGlobalServiceEvents") + is_global = self._get_bool_param("IncludeGlobalServiceEvents", True) is_multi_region = self._get_bool_param("IsMultiRegionTrail", False) if not is_global and is_multi_region: raise InvalidParameterCombinationException( @@ -27,14 +27,23 @@ class CloudTrailResponse(BaseResponse): sns_topic_name = self._get_param("SnsTopicName") log_validation = self._get_bool_param("EnableLogFileValidation", False) is_org_trail = self._get_bool_param("IsOrganizationTrail", False) + cw_log_group_arn = self._get_param("CloudWatchLogsLogGroupArn") + cw_role_arn = self._get_param("CloudWatchLogsRoleArn") + kms_key_id = self._get_param("KmsKeyId") + tags_list = self._get_param("TagsList", []) trail = self.cloudtrail_backend.create_trail( name, bucket_name, s3_key_prefix, sns_topic_name, + is_global, is_multi_region, log_validation, is_org_trail, + cw_log_group_arn, + cw_role_arn, + kms_key_id, + tags_list, ) return json.dumps(trail.description()) @@ -73,3 +82,111 @@ class CloudTrailResponse(BaseResponse): name = self._get_param("Name") self.cloudtrail_backend.delete_trail(name) return json.dumps({}) + + def update_trail(self): + name = self._get_param("Name") + s3_bucket_name = self._get_param("S3BucketName") + s3_key_prefix = self._get_param("S3KeyPrefix") + sns_topic_name = self._get_param("SnsTopicName") + include_global_service_events = self._get_param("IncludeGlobalServiceEvents") + is_multi_region_trail = self._get_param("IsMultiRegionTrail") + enable_log_file_validation = self._get_param("EnableLogFileValidation") + is_organization_trail = self._get_param("IsOrganizationTrail") + cw_log_group_arn = self._get_param("CloudWatchLogsLogGroupArn") + cw_role_arn = self._get_param("CloudWatchLogsRoleArn") + kms_key_id = self._get_param("KmsKeyId") + trail = self.cloudtrail_backend.update_trail( + name=name, + s3_bucket_name=s3_bucket_name, + s3_key_prefix=s3_key_prefix, + sns_topic_name=sns_topic_name, + include_global_service_events=include_global_service_events, + is_multi_region_trail=is_multi_region_trail, + enable_log_file_validation=enable_log_file_validation, + is_organization_trail=is_organization_trail, + cw_log_group_arn=cw_log_group_arn, + cw_role_arn=cw_role_arn, + kms_key_id=kms_key_id, + ) + return json.dumps(trail.description()) + + def put_event_selectors(self): + params = json.loads(self.body) + trail_name = params.get("TrailName") + event_selectors = params.get("EventSelectors") + advanced_event_selectors = params.get("AdvancedEventSelectors") + ( + trail_arn, + event_selectors, + advanced_event_selectors, + ) = self.cloudtrail_backend.put_event_selectors( + trail_name=trail_name, + event_selectors=event_selectors, + advanced_event_selectors=advanced_event_selectors, + ) + return json.dumps( + dict( + TrailARN=trail_arn, + EventSelectors=event_selectors, + AdvancedEventSelectors=advanced_event_selectors, + ) + ) + + def get_event_selectors(self): + params = json.loads(self.body) + trail_name = params.get("TrailName") + ( + trail_arn, + event_selectors, + advanced_event_selectors, + ) = self.cloudtrail_backend.get_event_selectors(trail_name=trail_name,) + return json.dumps( + dict( + TrailARN=trail_arn, + EventSelectors=event_selectors, + AdvancedEventSelectors=advanced_event_selectors, + ) + ) + + def add_tags(self): + params = json.loads(self.body) + resource_id = params.get("ResourceId") + tags_list = params.get("TagsList") + self.cloudtrail_backend.add_tags( + resource_id=resource_id, tags_list=tags_list, + ) + return json.dumps(dict()) + + def remove_tags(self): + resource_id = self._get_param("ResourceId") + tags_list = self._get_param("TagsList") + self.cloudtrail_backend.remove_tags( + resource_id=resource_id, tags_list=tags_list, + ) + return json.dumps(dict()) + + def list_tags(self): + params = json.loads(self.body) + resource_id_list = params.get("ResourceIdList") + resource_tag_list = self.cloudtrail_backend.list_tags( + resource_id_list=resource_id_list, + ) + return json.dumps(dict(ResourceTagList=resource_tag_list)) + + def put_insight_selectors(self): + trail_name = self._get_param("TrailName") + insight_selectors = self._get_param("InsightSelectors") + trail_arn, insight_selectors = self.cloudtrail_backend.put_insight_selectors( + trail_name=trail_name, insight_selectors=insight_selectors, + ) + return json.dumps(dict(TrailARN=trail_arn, InsightSelectors=insight_selectors)) + + def get_insight_selectors(self): + trail_name = self._get_param("TrailName") + trail_arn, insight_selectors = self.cloudtrail_backend.get_insight_selectors( + trail_name=trail_name, + ) + resp = {"TrailARN": trail_arn} + if insight_selectors: + resp["InsightSelectors"] = insight_selectors + return json.dumps(resp) diff --git a/moto/organizations/models.py b/moto/organizations/models.py index 97bcb0224..d3579e744 100644 --- a/moto/organizations/models.py +++ b/moto/organizations/models.py @@ -30,7 +30,9 @@ class FakeOrganization(BaseModel): self.master_account_id = utils.MASTER_ACCOUNT_ID self.master_account_email = utils.MASTER_ACCOUNT_EMAIL self.available_policy_types = [ - # TODO: verify if this should be enabled by default (breaks TF tests for CloudTrail) + # This policy is available, but not applied + # User should use enable_policy_type/disable_policy_type to do anything else + # This field is deprecated in AWS, but we'll return it for old time's sake {"Type": "SERVICE_CONTROL_POLICY", "Status": "ENABLED"} ] @@ -140,10 +142,7 @@ class FakeRoot(FakeOrganizationalUnit): self.type = "ROOT" self.id = organization.root_id self.name = "Root" - self.policy_types = [ - # TODO: verify if this should be enabled by default (breaks TF tests for CloudTrail) - {"Type": "SERVICE_CONTROL_POLICY", "Status": "ENABLED"} - ] + self.policy_types = [] self._arn_format = utils.ROOT_ARN_FORMAT self.attached_policies = [] self.tags = {tag["Key"]: tag["Value"] for tag in kwargs.get("Tags", [])} diff --git a/tests/terraform-tests.success.txt b/tests/terraform-tests.success.txt index aa2e68b28..fdb32be77 100644 --- a/tests/terraform-tests.success.txt +++ b/tests/terraform-tests.success.txt @@ -11,6 +11,7 @@ TestAccAWSAppsyncGraphqlApi TestAccAWSAvailabilityZones TestAccAWSBillingServiceAccount TestAccAWSCallerIdentity +TestAccAWSCloudTrail TestAccAWSCloudTrailServiceAccount TestAccAWSCloudWatchDashboard TestAccAWSCloudWatchEventApiDestination diff --git a/tests/test_cloudtrail/test_cloudtrail.py b/tests/test_cloudtrail/test_cloudtrail.py index 89e8118f4..23e1b9fe6 100644 --- a/tests/test_cloudtrail/test_cloudtrail.py +++ b/tests/test_cloudtrail/test_cloudtrail.py @@ -147,7 +147,9 @@ def test_create_trail_advanced(): ) resp.should.have.key("LogFileValidationEnabled").equal(True) resp.should.have.key("IsOrganizationTrail").equal(True) - return resp + resp.should.have.key("CloudWatchLogsLogGroupArn").equals("cwllga") + resp.should.have.key("CloudWatchLogsRoleArn").equals("cwlra") + resp.should.have.key("KmsKeyId").equals("kki") def create_trail_advanced(region_name="us-east-1"): @@ -168,6 +170,9 @@ def create_trail_advanced(region_name="us-east-1"): IsMultiRegionTrail=True, EnableLogFileValidation=True, IsOrganizationTrail=True, + CloudWatchLogsLogGroupArn="cwllga", + CloudWatchLogsRoleArn="cwlra", + KmsKeyId="kki", TagsList=[{"Key": "tk", "Value": "tv"}, {"Key": "tk2", "Value": "tv2"}], ) return bucket_name, resp, sns_topic_name, trail_name @@ -461,3 +466,72 @@ def test_delete_trail(): trails = client.describe_trails()["trailList"] trails.should.have.length_of(0) + + +@mock_cloudtrail +@mock_s3 +def test_update_trail_simple(): + client = boto3.client("cloudtrail", region_name="ap-southeast-2") + bucket_name, trail, name = create_trail_simple(region_name="ap-southeast-2") + resp = client.update_trail(Name=name) + resp.should.have.key("Name").equal(name) + resp.should.have.key("S3BucketName").equal(bucket_name) + resp.should.have.key("IncludeGlobalServiceEvents").equal(True) + resp.should.have.key("IsMultiRegionTrail").equal(False) + resp.should.have.key("LogFileValidationEnabled").equal(False) + resp.should.have.key("IsOrganizationTrail").equal(False) + resp.shouldnt.have.key("S3KeyPrefix") + resp.shouldnt.have.key("SnsTopicName") + resp.shouldnt.have.key("SnsTopicARN") + + trail = client.get_trail(Name=name)["Trail"] + trail.should.have.key("Name").equal(name) + trail.should.have.key("S3BucketName").equal(bucket_name) + trail.should.have.key("IncludeGlobalServiceEvents").equal(True) + trail.should.have.key("IsMultiRegionTrail").equal(False) + trail.should.have.key("LogFileValidationEnabled").equal(False) + trail.should.have.key("IsOrganizationTrail").equal(False) + trail.shouldnt.have.key("S3KeyPrefix") + trail.shouldnt.have.key("SnsTopicName") + trail.shouldnt.have.key("SnsTopicARN") + + +@mock_cloudtrail +@mock_s3 +def test_update_trail_full(): + client = boto3.client("cloudtrail", region_name="ap-southeast-1") + _, trail, name = create_trail_simple(region_name="ap-southeast-1") + resp = client.update_trail( + Name=name, + S3BucketName="updated_bucket", + S3KeyPrefix="s3kp", + SnsTopicName="stn", + IncludeGlobalServiceEvents=False, + IsMultiRegionTrail=True, + EnableLogFileValidation=True, + CloudWatchLogsLogGroupArn="cwllga", + CloudWatchLogsRoleArn="cwlra", + KmsKeyId="kki", + IsOrganizationTrail=True, + ) + resp.should.have.key("Name").equal(name) + resp.should.have.key("S3BucketName").equal("updated_bucket") + resp.should.have.key("S3KeyPrefix").equals("s3kp") + resp.should.have.key("SnsTopicName").equals("stn") + resp.should.have.key("IncludeGlobalServiceEvents").equal(False) + resp.should.have.key("IsMultiRegionTrail").equal(True) + resp.should.have.key("LogFileValidationEnabled").equal(True) + resp.should.have.key("IsOrganizationTrail").equal(True) + + trail = client.get_trail(Name=name)["Trail"] + trail.should.have.key("Name").equal(name) + trail.should.have.key("S3BucketName").equal("updated_bucket") + trail.should.have.key("S3KeyPrefix").equals("s3kp") + trail.should.have.key("SnsTopicName").equals("stn") + trail.should.have.key("IncludeGlobalServiceEvents").equal(False) + trail.should.have.key("IsMultiRegionTrail").equal(True) + trail.should.have.key("LogFileValidationEnabled").equal(True) + trail.should.have.key("IsOrganizationTrail").equal(True) + trail.should.have.key("CloudWatchLogsLogGroupArn").equals("cwllga") + trail.should.have.key("CloudWatchLogsRoleArn").equals("cwlra") + trail.should.have.key("KmsKeyId").equals("kki") diff --git a/tests/test_cloudtrail/test_cloudtrail_eventselectors.py b/tests/test_cloudtrail/test_cloudtrail_eventselectors.py new file mode 100644 index 000000000..e7f1b3c3f --- /dev/null +++ b/tests/test_cloudtrail/test_cloudtrail_eventselectors.py @@ -0,0 +1,208 @@ +import boto3 +import pytest + +from moto import mock_cloudtrail, mock_s3 +from moto.core import ACCOUNT_ID + +from .test_cloudtrail import create_trail_simple + + +@mock_cloudtrail +@mock_s3 +def test_put_event_selectors(): + client = boto3.client("cloudtrail", region_name="eu-west-1") + _, _, trail_name = create_trail_simple(region_name="eu-west-1") + + resp = client.put_event_selectors( + TrailName=trail_name, + EventSelectors=[ + { + "ReadWriteType": "All", + "IncludeManagementEvents": True, + "DataResources": [ + {"Type": "AWS::S3::Object", "Values": ["arn:aws:s3:::*/*"]} + ], + } + ], + ) + + resp.should.have.key("TrailARN") + resp.should.have.key("EventSelectors").equals( + [ + { + "ReadWriteType": "All", + "IncludeManagementEvents": True, + "DataResources": [ + {"Type": "AWS::S3::Object", "Values": ["arn:aws:s3:::*/*"]} + ], + } + ] + ) + resp.shouldnt.have.key("AdvancedEventSelectors") + + +@mock_cloudtrail +@mock_s3 +def test_put_event_selectors_advanced(): + client = boto3.client("cloudtrail", region_name="eu-west-1") + _, _, trail_name = create_trail_simple(region_name="eu-west-1") + + resp = client.put_event_selectors( + TrailName=trail_name, + EventSelectors=[ + { + "ReadWriteType": "All", + "IncludeManagementEvents": True, + "DataResources": [ + {"Type": "AWS::S3::Object", "Values": ["arn:aws:s3:::*/*"]} + ], + } + ], + AdvancedEventSelectors=[ + {"Name": "aes1", "FieldSelectors": [{"Field": "f", "Equals": ["fs1"]}]} + ], + ) + + resp.should.have.key("TrailARN") + resp.should.have.key("EventSelectors").equals( + [ + { + "ReadWriteType": "All", + "IncludeManagementEvents": True, + "DataResources": [ + {"Type": "AWS::S3::Object", "Values": ["arn:aws:s3:::*/*"]} + ], + } + ] + ) + resp.should.have.key("AdvancedEventSelectors").equals( + [{"Name": "aes1", "FieldSelectors": [{"Field": "f", "Equals": ["fs1"]}]}] + ) + + +@mock_cloudtrail +@mock_s3 +def test_get_event_selectors_empty(): + client = boto3.client("cloudtrail", region_name="ap-southeast-1") + _, _, trail_name = create_trail_simple(region_name="ap-southeast-1") + + resp = client.get_event_selectors(TrailName=trail_name) + + resp.should.have.key("TrailARN").equals( + f"arn:aws:cloudtrail:ap-southeast-1:{ACCOUNT_ID}:trail/{trail_name}" + ) + resp.should.have.key("EventSelectors").equals([]) + resp.should.have.key("AdvancedEventSelectors").equals([]) + + +@mock_cloudtrail +@mock_s3 +def test_get_event_selectors(): + client = boto3.client("cloudtrail", region_name="ap-southeast-2") + _, _, trail_name = create_trail_simple(region_name="ap-southeast-2") + + client.put_event_selectors( + TrailName=trail_name, + EventSelectors=[ + { + "ReadWriteType": "All", + "IncludeManagementEvents": False, + "DataResources": [ + {"Type": "AWS::S3::Object", "Values": ["arn:aws:s3:::*/*"]} + ], + } + ], + ) + + resp = client.get_event_selectors(TrailName=trail_name) + + resp.should.have.key("TrailARN").equals( + f"arn:aws:cloudtrail:ap-southeast-2:{ACCOUNT_ID}:trail/{trail_name}" + ) + resp.should.have.key("EventSelectors").equals( + [ + { + "ReadWriteType": "All", + "IncludeManagementEvents": False, + "DataResources": [ + {"Type": "AWS::S3::Object", "Values": ["arn:aws:s3:::*/*"]} + ], + } + ] + ) + + +@mock_cloudtrail +@mock_s3 +def test_get_event_selectors_multiple(): + client = boto3.client("cloudtrail", region_name="ap-southeast-1") + _, _, trail_name = create_trail_simple(region_name="ap-southeast-1") + + client.put_event_selectors( + TrailName=trail_name, + EventSelectors=[ + { + "ReadWriteType": "All", + "IncludeManagementEvents": False, + "DataResources": [ + {"Type": "AWS::S3::Object", "Values": ["arn:aws:s3:::*/*"]} + ], + } + ], + ) + + client.put_event_selectors( + TrailName=trail_name, + AdvancedEventSelectors=[ + {"Name": "aes1", "FieldSelectors": [{"Field": "f", "Equals": ["fs1"]}]} + ], + ) + + resp = client.get_event_selectors(TrailName=trail_name) + + resp.should.have.key("TrailARN") + # Setting advanced selectors cancels any existing event selectors + resp.should.have.key("EventSelectors").equals([]) + resp.should.have.key("AdvancedEventSelectors").length_of(1) + resp.should.have.key("AdvancedEventSelectors").equals( + [{"Name": "aes1", "FieldSelectors": [{"Field": "f", "Equals": ["fs1"]}]}] + ) + + +@mock_cloudtrail +@mock_s3 +@pytest.mark.parametrize("using_arn", [True, False]) +def test_put_insight_selectors(using_arn): + client = boto3.client("cloudtrail", region_name="us-east-2") + _, resp, trail_name = create_trail_simple(region_name="us-east-2") + + resp = client.put_insight_selectors( + TrailName=trail_name, InsightSelectors=[{"InsightType": "ApiCallRateInsight"}] + ) + + resp.should.have.key("TrailARN") + resp.should.have.key("InsightSelectors").equals( + [{"InsightType": "ApiCallRateInsight"}] + ) + + if using_arn: + trail_arn = resp["TrailARN"] + resp = client.get_insight_selectors(TrailName=trail_arn) + else: + resp = client.get_insight_selectors(TrailName=trail_name) + + resp.should.have.key("TrailARN") + resp.should.have.key("InsightSelectors").equals( + [{"InsightType": "ApiCallRateInsight"}] + ) + + +@mock_cloudtrail +@mock_s3 +def test_get_insight_selectors(): + client = boto3.client("cloudtrail", region_name="eu-west-1") + _, resp, trail_name = create_trail_simple(region_name="eu-west-1") + resp = client.get_insight_selectors(TrailName=trail_name) + + resp.should.have.key("TrailARN") + resp.shouldnt.have.key("InsightSelectors") diff --git a/tests/test_cloudtrail/test_cloudtrail_tags.py b/tests/test_cloudtrail/test_cloudtrail_tags.py new file mode 100644 index 000000000..41f04a686 --- /dev/null +++ b/tests/test_cloudtrail/test_cloudtrail_tags.py @@ -0,0 +1,77 @@ +import boto3 + +from moto import mock_cloudtrail, mock_s3, mock_sns + +from .test_cloudtrail import create_trail_simple, create_trail_advanced + + +@mock_cloudtrail +@mock_s3 +def test_add_tags(): + client = boto3.client("cloudtrail", region_name="ap-southeast-1") + _, resp, _ = create_trail_simple(region_name="ap-southeast-1") + trail_arn = resp["TrailARN"] + + client.add_tags(ResourceId=trail_arn, TagsList=[{"Key": "k1", "Value": "v1"},]) + + resp = client.list_tags(ResourceIdList=[trail_arn]) + resp.should.have.key("ResourceTagList").length_of(1) + resp["ResourceTagList"][0].should.equal( + {"ResourceId": trail_arn, "TagsList": [{"Key": "k1", "Value": "v1"},]} + ) + + +@mock_cloudtrail +@mock_s3 +@mock_sns +def test_remove_tags(): + client = boto3.client("cloudtrail", region_name="ap-southeast-1") + # Start with two tags + _, resp, _, _ = create_trail_advanced(region_name="ap-southeast-1") + trail_arn = resp["TrailARN"] + + # Add a third tag + client.add_tags(ResourceId=trail_arn, TagsList=[{"Key": "tk3", "Value": "tv3"},]) + + # Remove the second tag + client.remove_tags(ResourceId=trail_arn, TagsList=[{"Key": "tk2", "Value": "tv2"}]) + + # Verify the first and third tag are still there + resp = client.list_tags(ResourceIdList=[trail_arn]) + resp.should.have.key("ResourceTagList").length_of(1) + resp["ResourceTagList"][0].should.equal( + { + "ResourceId": trail_arn, + "TagsList": [{"Key": "tk", "Value": "tv"}, {"Key": "tk3", "Value": "tv3"}], + } + ) + + +@mock_cloudtrail +@mock_s3 +def test_create_trail_without_tags_and_list_tags(): + client = boto3.client("cloudtrail", region_name="us-east-2") + _, resp, _ = create_trail_simple(region_name="us-east-2") + trail_arn = resp["TrailARN"] + + resp = client.list_tags(ResourceIdList=[trail_arn]) + resp.should.have.key("ResourceTagList").length_of(1) + resp["ResourceTagList"][0].should.equal({"ResourceId": trail_arn, "TagsList": []}) + + +@mock_cloudtrail +@mock_s3 +@mock_sns +def test_create_trail_with_tags_and_list_tags(): + client = boto3.client("cloudtrail", region_name="us-east-2") + _, resp, _, _ = create_trail_advanced(region_name="us-east-2") + trail_arn = resp["TrailARN"] + + resp = client.list_tags(ResourceIdList=[trail_arn]) + resp.should.have.key("ResourceTagList").length_of(1) + resp["ResourceTagList"][0].should.equal( + { + "ResourceId": trail_arn, + "TagsList": [{"Key": "tk", "Value": "tv"}, {"Key": "tk2", "Value": "tv2"}], + } + ) diff --git a/tests/test_organizations/organizations_test_utils.py b/tests/test_organizations/organizations_test_utils.py index f935cea7c..9dc6af40e 100644 --- a/tests/test_organizations/organizations_test_utils.py +++ b/tests/test_organizations/organizations_test_utils.py @@ -70,11 +70,7 @@ def validate_roots(org, response): utils.ROOT_ARN_FORMAT.format(org["MasterAccountId"], org["Id"], root["Id"]) ) root.should.have.key("Name").should.be.a(str) - root.should.have.key("PolicyTypes").should.be.a(list) - root["PolicyTypes"][0].should.have.key("Type").should.equal( - "SERVICE_CONTROL_POLICY" - ) - root["PolicyTypes"][0].should.have.key("Status").should.equal("ENABLED") + root.should.have.key("PolicyTypes").should.equal([]) def validate_organizational_unit(org, response): diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py index 6914fadec..d263dbc02 100644 --- a/tests/test_organizations/test_organizations_boto3.py +++ b/tests/test_organizations/test_organizations_boto3.py @@ -1812,10 +1812,7 @@ def test_enable_policy_type(): ) root["Name"].should.equal("Root") sorted(root["PolicyTypes"], key=lambda x: x["Type"]).should.equal( - [ - {"Type": "AISERVICES_OPT_OUT_POLICY", "Status": "ENABLED"}, - {"Type": "SERVICE_CONTROL_POLICY", "Status": "ENABLED"}, - ] + [{"Type": "AISERVICES_OPT_OUT_POLICY", "Status": "ENABLED"}] ) @@ -1842,7 +1839,10 @@ def test_enable_policy_type_errors(): "You specified a root that doesn't exist." ) - # enable policy again ('SERVICE_CONTROL_POLICY' is enabled by default) + # enable policy again + # given + client.enable_policy_type(RootId=root_id, PolicyType="SERVICE_CONTROL_POLICY") + # when with pytest.raises(ClientError) as e: client.enable_policy_type(RootId=root_id, PolicyType="SERVICE_CONTROL_POLICY") @@ -1889,9 +1889,7 @@ def test_disable_policy_type(): utils.ROOT_ARN_FORMAT.format(org["MasterAccountId"], org["Id"], root_id) ) root["Name"].should.equal("Root") - root["PolicyTypes"].should.equal( - [{"Type": "SERVICE_CONTROL_POLICY", "Status": "ENABLED"}] - ) + root["PolicyTypes"].should.equal([]) @mock_organizations