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.core import BaseBackend, BaseModel
from moto.core.models import CloudFormationModel
from moto.core.utils import unix_time_millis
from moto.utilities.paginator import paginate
from moto.logs.metric_filters import MetricFilters
@ -540,6 +541,72 @@ class LogGroup(BaseModel):
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):
def __init__(self, region_name):
self.region_name = region_name
@ -741,29 +808,19 @@ class LogsBackend(BaseBackend):
"""
limit = limit or MAX_RESOURCE_POLICIES_PER_REGION
policies = []
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
return list(self.resource_policies.values())
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:
raise LimitExceededException()
policy = {
"policyName": policy_name,
"policyDocument": policy_doc,
"lastUpdatedTime": int(unix_time_millis()),
}
policy = LogResourcePolicy(policy_name, policy_doc)
self.resource_policies[policy_name] = policy
return {"resourcePolicy": policy}
return policy
def delete_resource_policy(self, policy_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")
limit = self._get_param("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):
policy_name = self._get_param("policyName")
policy_doc = self._get_param("policyDocument")
result = self.logs_backend.put_resource_policy(policy_name, policy_doc)
return json.dumps(result)
policy = self.logs_backend.put_resource_policy(policy_name, policy_doc)
return json.dumps({"resourcePolicy": policy.describe()})
def delete_resource_policy(self):
policy_name = self._get_param("policyName")

View File

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

View File

@ -1,6 +1,8 @@
from __future__ import unicode_literals
import boto
from unittest import SkipTest
from collections.abc import Iterable
from sure import assertion
def version_tuple(v):
@ -25,3 +27,22 @@ class requires_boto_gte(object):
if boto_version >= required:
return 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")
log_group = logs_conn.describe_log_groups()["logGroups"][0]
log_group["logGroupName"].should.equal("some-log-group")
log_group["retentionInDays"].should.be.equal(90)
@mock_cloudformation
@mock_logs
def test_create_cloudwatch_logs_resource_policy():
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

View File

@ -3,10 +3,12 @@ import os
import time
import sure # noqa
from unittest import SkipTest
from datetime import timedelta
import boto3
import pytest
from botocore.exceptions import ClientError
from freezegun import freeze_time
from moto import mock_logs, settings
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)
# 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
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"]["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
def test_delete_resource_policy(json_policy_doc):