feat(cloudformation): support logs resource policy (#4427)

This commit is contained in:
nom3ad 2021-10-18 14:47:31 +05:30 committed by GitHub
parent 080e7eba84
commit 0953c11b92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 220 additions and 27 deletions

View File

@ -4,6 +4,7 @@ from boto3 import Session
from moto import core as moto_core from moto import core as moto_core
from moto.core import BaseBackend, BaseModel from moto.core import BaseBackend, BaseModel
from moto.core.models import CloudFormationModel
from moto.core.utils import unix_time_millis from moto.core.utils import unix_time_millis
from moto.utilities.paginator import paginate from moto.utilities.paginator import paginate
from moto.logs.metric_filters import MetricFilters from moto.logs.metric_filters import MetricFilters
@ -540,6 +541,72 @@ class LogGroup(BaseModel):
self.subscription_filters = [] self.subscription_filters = []
class LogResourcePolicy(CloudFormationModel):
def __init__(self, policy_name, policy_document):
self.policy_name = policy_name
self.policy_document = policy_document
self.last_updated_time = int(unix_time_millis())
def update(self, policy_document):
self.policy_document = policy_document
self.last_updated_time = int(unix_time_millis())
def describe(self):
return {
"policyName": self.policy_name,
"policyDocument": self.policy_document,
"lastUpdatedTime": self.last_updated_time,
}
@property
def physical_resource_id(self):
return self.policy_name
@staticmethod
def cloudformation_name_type():
return "PolicyName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-resourcepolicy.html
return "AWS::Logs::ResourcePolicy"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
):
properties = cloudformation_json["Properties"]
policy_name = properties["PolicyName"]
policy_document = properties["PolicyDocument"]
return logs_backends[region_name].put_resource_policy(
policy_name, policy_document
)
@classmethod
def update_from_cloudformation_json(
cls, original_resource, new_resource_name, cloudformation_json, region_name
):
properties = cloudformation_json["Properties"]
policy_name = properties["PolicyName"]
policy_document = properties["PolicyDocument"]
updated = logs_backends[region_name].put_resource_policy(
policy_name, policy_document
)
# TODO: move `update by replacement logic` to cloudformation. this is required for implementing rollbacks
if original_resource.policy_name != policy_name:
logs_backends[region_name].delete_resource_policy(
original_resource.policy_name
)
return updated
@classmethod
def delete_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
):
return logs_backends[region_name].delete_resource_policy(resource_name)
class LogsBackend(BaseBackend): class LogsBackend(BaseBackend):
def __init__(self, region_name): def __init__(self, region_name):
self.region_name = region_name self.region_name = region_name
@ -741,29 +808,19 @@ class LogsBackend(BaseBackend):
""" """
limit = limit or MAX_RESOURCE_POLICIES_PER_REGION limit = limit or MAX_RESOURCE_POLICIES_PER_REGION
policies = [] return list(self.resource_policies.values())
for policy_name, policy_info in self.resource_policies.items():
policies.append(
{
"policyName": policy_name,
"policyDocument": policy_info["policyDocument"],
"lastUpdatedTime": policy_info["lastUpdatedTime"],
}
)
return policies
def put_resource_policy(self, policy_name, policy_doc): def put_resource_policy(self, policy_name, policy_doc):
"""Create resource policy and return dict of policy name and doc.""" """Creates/updates resource policy and return policy object"""
if policy_name in self.resource_policies:
policy = self.resource_policies[policy_name]
policy.update(policy_doc)
return policy
if len(self.resource_policies) == MAX_RESOURCE_POLICIES_PER_REGION: if len(self.resource_policies) == MAX_RESOURCE_POLICIES_PER_REGION:
raise LimitExceededException() raise LimitExceededException()
policy = LogResourcePolicy(policy_name, policy_doc)
policy = {
"policyName": policy_name,
"policyDocument": policy_doc,
"lastUpdatedTime": int(unix_time_millis()),
}
self.resource_policies[policy_name] = policy self.resource_policies[policy_name] = policy
return {"resourcePolicy": policy} return policy
def delete_resource_policy(self, policy_name): def delete_resource_policy(self, policy_name):
"""Remove resource policy with a policy name matching given name.""" """Remove resource policy with a policy name matching given name."""

View File

@ -293,13 +293,13 @@ class LogsResponse(BaseResponse):
next_token = self._get_param("nextToken") next_token = self._get_param("nextToken")
limit = self._get_param("limit") limit = self._get_param("limit")
policies = self.logs_backend.describe_resource_policies(next_token, limit) policies = self.logs_backend.describe_resource_policies(next_token, limit)
return json.dumps({"resourcePolicies": policies}) return json.dumps({"resourcePolicies": [p.describe() for p in policies]})
def put_resource_policy(self): def put_resource_policy(self):
policy_name = self._get_param("policyName") policy_name = self._get_param("policyName")
policy_doc = self._get_param("policyDocument") policy_doc = self._get_param("policyDocument")
result = self.logs_backend.put_resource_policy(policy_name, policy_doc) policy = self.logs_backend.put_resource_policy(policy_name, policy_doc)
return json.dumps(result) return json.dumps({"resourcePolicy": policy.describe()})
def delete_resource_policy(self): def delete_resource_policy(self):
policy_name = self._get_param("policyName") policy_name = self._get_param("policyName")

View File

@ -2,6 +2,8 @@ from __future__ import unicode_literals
import logging import logging
from . import helpers # noqa
# Disable extra logging for tests # Disable extra logging for tests
logging.getLogger("boto").setLevel(logging.CRITICAL) logging.getLogger("boto").setLevel(logging.CRITICAL)
logging.getLogger("boto3").setLevel(logging.CRITICAL) logging.getLogger("boto3").setLevel(logging.CRITICAL)

View File

@ -1,6 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import boto import boto
from unittest import SkipTest from unittest import SkipTest
from collections.abc import Iterable
from sure import assertion
def version_tuple(v): def version_tuple(v):
@ -25,3 +27,22 @@ class requires_boto_gte(object):
if boto_version >= required: if boto_version >= required:
return test return test
return skip_test return skip_test
@assertion
def containing_item_with_attributes(context, **kwargs):
contains = False
if kwargs and isinstance(context.obj, Iterable):
for item in context.obj:
if not isinstance(item, dict):
continue
for k, v in kwargs.items():
if k not in item or item[k] != v:
break
else:
contains = True
if context.negative:
assert not contains, f"{context.obj} contains matching item {kwargs}"
else:
assert contains, f"{context.obj} does not contain matching item {kwargs}"
return True

View File

@ -2849,13 +2849,108 @@ def test_create_log_group_using_fntransform():
}, },
} }
cf_conn = boto3.client("cloudformation", "us-west-2")
cf_conn.create_stack(StackName="test_stack", TemplateBody=json.dumps(template))
logs_conn = boto3.client("logs", region_name="us-west-2") @mock_cloudformation
log_group = logs_conn.describe_log_groups()["logGroups"][0] @mock_logs
log_group["logGroupName"].should.equal("some-log-group") def test_create_cloudwatch_logs_resource_policy():
log_group["retentionInDays"].should.be.equal(90) policy_document = json.dumps(
{
"Statement": [
{
"Action": ["logs:CreateLogStream", "logs:PutLogEvents",],
"Effect": "Allow",
"Principal": {"Service": "es.amazonaws.com"},
"Resource": "*",
}
]
}
)
template1 = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {
"LogGroupPolicy1": {
"Type": "AWS::Logs::ResourcePolicy",
"Properties": {
"PolicyDocument": policy_document,
"PolicyName": "TestPolicyA",
},
}
},
}
template2 = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {
"LogGroupPolicy1": {
"Type": "AWS::Logs::ResourcePolicy",
"Properties": {
"PolicyDocument": policy_document,
"PolicyName": "TestPolicyB",
},
},
"LogGroupPolicy2": {
"Type": "AWS::Logs::ResourcePolicy",
"Properties": {
"PolicyDocument": policy_document,
"PolicyName": "TestPolicyC",
},
},
},
}
cf_conn = boto3.client("cloudformation", "us-east-1")
cf_conn.create_stack(StackName="test_stack", TemplateBody=json.dumps(template1))
logs_conn = boto3.client("logs", region_name="us-east-1")
policies = logs_conn.describe_resource_policies()["resourcePolicies"]
policies.should.have.length_of(1)
policies.should.be.containing_item_with_attributes(
policyName="TestPolicyA", policyDocument=policy_document
)
cf_conn.update_stack(StackName="test_stack", TemplateBody=json.dumps(template2))
policies = logs_conn.describe_resource_policies()["resourcePolicies"]
policies.should.have.length_of(2)
policies.should.be.containing_item_with_attributes(
policyName="TestPolicyB", policyDocument=policy_document
)
policies.should.be.containing_item_with_attributes(
policyName="TestPolicyC", policyDocument=policy_document
)
cf_conn.update_stack(StackName="test_stack", TemplateBody=json.dumps(template1))
policies = logs_conn.describe_resource_policies()["resourcePolicies"]
policies.should.have.length_of(1)
policies.should.be.containing_item_with_attributes(
policyName="TestPolicyA", policyDocument=policy_document
)
@mock_cloudformation
@mock_logs
def test_delete_stack_containing_cloudwatch_logs_resource_policy():
template1 = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {
"LogGroupPolicy1": {
"Type": "AWS::Logs::ResourcePolicy",
"Properties": {
"PolicyDocument": '{"Statement":[{"Action":"logs:*","Effect":"Allow","Principal":"*","Resource":"*"}]}',
"PolicyName": "TestPolicyA",
},
}
},
}
cf_conn = boto3.client("cloudformation", "us-east-1")
cf_conn.create_stack(StackName="test_stack", TemplateBody=json.dumps(template1))
logs_conn = boto3.client("logs", region_name="us-east-1")
policies = logs_conn.describe_resource_policies()["resourcePolicies"]
policies.should.have.length_of(1)
cf_conn.delete_stack(StackName="test_stack")
policies = logs_conn.describe_resource_policies()["resourcePolicies"]
policies.should.have.length_of(0)
@mock_cloudformation @mock_cloudformation

View File

@ -3,10 +3,12 @@ import os
import time import time
import sure # noqa import sure # noqa
from unittest import SkipTest from unittest import SkipTest
from datetime import timedelta
import boto3 import boto3
import pytest import pytest
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
from freezegun import freeze_time
from moto import mock_logs, settings from moto import mock_logs, settings
from moto.core.utils import unix_time_millis from moto.core.utils import unix_time_millis
@ -592,6 +594,17 @@ def test_put_resource_policy():
client.delete_log_group(logGroupName=log_group_name) client.delete_log_group(logGroupName=log_group_name)
# put_resource_policy with same policy name should update the resouce
created_time = response["resourcePolicy"]["lastUpdatedTime"]
with freeze_time(timedelta(minutes=1)):
new_document = '{"Statement":[{"Action":"logs:*","Effect":"Allow","Principal":"*","Resource":"*"}]}'
policy_info = client.put_resource_policy(
policyName=policy_name, policyDocument=new_document,
)["resourcePolicy"]
assert policy_info["policyName"] == policy_name
assert policy_info["policyDocument"] == new_document
assert created_time < policy_info["lastUpdatedTime"] <= int(unix_time_millis())
@mock_logs @mock_logs
def test_put_resource_policy_too_many(json_policy_doc): def test_put_resource_policy_too_many(json_policy_doc):
@ -615,6 +628,11 @@ def test_put_resource_policy_too_many(json_policy_doc):
exc_value.response["Error"]["Code"].should.equal("LimitExceededException") exc_value.response["Error"]["Code"].should.equal("LimitExceededException")
exc_value.response["Error"]["Message"].should.contain("Resource limit exceeded.") exc_value.response["Error"]["Message"].should.contain("Resource limit exceeded.")
# put_resource_policy on already created policy, shouldnt throw any error
client.put_resource_policy(
policyName="test_policy_1", policyDocument=json.dumps(json_policy_doc)
)
@mock_logs @mock_logs
def test_delete_resource_policy(json_policy_doc): def test_delete_resource_policy(json_policy_doc):