From 5708d083991c5643e0d07683c0f1d77375f947f6 Mon Sep 17 00:00:00 2001 From: Rob Young Date: Thu, 6 Jul 2017 13:30:52 +0100 Subject: [PATCH] Allow multi-part policies on aditional s3 buckets AIM policy documents are made up of a list of policy statements. Currently bootstrap-cfn only allows a single custom policy for an S3 bucket. This change allows a list of custom policies to be provided. Backwards compatibility is maintained by allowing either a single policy dict or a list of policies as an array. I have only implemented this on the additional s3 buckets because the static buckets are for a specific use case and should be kept as simple as possible. --- README.rst | 3 + bootstrap_cfn/config.py | 5 +- ...le-s3-additional-bucket-custom-policy.json | 6 ++ ...ional-bucket-multi-part-custom-policy.json | 22 +++++ tests/tests.py | 82 ++++++++++++++++++- 5 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 tests/sample-s3-additional-bucket-custom-policy.json create mode 100644 tests/sample-s3-additional-bucket-multi-part-custom-policy.json diff --git a/README.rst b/README.rst index b983732..ac93995 100644 --- a/README.rst +++ b/README.rst @@ -586,6 +586,8 @@ For example, the sample custom policy defined in this `json file `_ .. code:: yaml @@ -602,6 +604,7 @@ These entries can also specify their own policies or use the default, vpc limite lifecycles: /: expirationdays: 5 + policy: tests/sample-s3-additional-bucket-multi-part-custom-policy.json The outputs of these buckets will be the bucket name postfixed by 'BucketName', ie, mybucketidBucketName. Additionally, and as shown above, one can define a list of Lifecycle rules on a per prefix basis. If a root rule is defined, the rest of the rules are ignored. diff --git a/bootstrap_cfn/config.py b/bootstrap_cfn/config.py index 5e8dfbe..4a86ac9 100644 --- a/bootstrap_cfn/config.py +++ b/bootstrap_cfn/config.py @@ -546,10 +546,13 @@ def create_s3_bucket(self, bucket_config, template): } } } + + policy = policy if isinstance(policy, list) else [policy] + bucket_policy = BucketPolicy( "{}Policy".format(bucket_name), Bucket=Ref(bucket), - PolicyDocument={"Statement": [policy]}, + PolicyDocument={"Statement": policy}, ) # Add the bucket name to the list of cloudformation # outputs diff --git a/tests/sample-s3-additional-bucket-custom-policy.json b/tests/sample-s3-additional-bucket-custom-policy.json new file mode 100644 index 0000000..6560407 --- /dev/null +++ b/tests/sample-s3-additional-bucket-custom-policy.json @@ -0,0 +1,6 @@ +{ + "Action": ["s3:Get*", "s3:Put*", "s3:List*", "s3:Delete*"], + "Resource": "arn:aws:s3:::testbucket/*", + "Effect": "Allow", + "Principal": {"AWS": "*"} +} diff --git a/tests/sample-s3-additional-bucket-multi-part-custom-policy.json b/tests/sample-s3-additional-bucket-multi-part-custom-policy.json new file mode 100644 index 0000000..773af6e --- /dev/null +++ b/tests/sample-s3-additional-bucket-multi-part-custom-policy.json @@ -0,0 +1,22 @@ +[ + { + "Action": ["s3:Get*", "s3:Put*", "s3:List*", "s3:Delete*"], + "Resource": "arn:aws:s3:::testbucket/*", + "Effect": "Allow", + "Condition": { + "StringEquals": { + "aws:sourceVpc": {"Ref": "VPC"} + } + } + }, + { + "Action": ["s3:Put*"], + "Resource": "arn:aws:s3:::testbucket/*", + "Effect": "Deny", + "Condition": { + "StringNotEquals": { + "s3:x-amz-server-side-encryption": "AES256" + } + } + } +] diff --git a/tests/tests.py b/tests/tests.py index 9a45873..eff8b86 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -249,7 +249,7 @@ def test_s3_no_subkeys(self): actual_outputs = self._resources_to_dict(template.outputs.values()) compare(expected_outputs, actual_outputs) - def test_custom_s3_policy(self): + def test_s3_custom_policy(self): resource_value = 'arn:aws:s3:::moj-test-dev-static/*' expected_s3 = [ { @@ -458,6 +458,86 @@ def _policy(x): self._resources_to_dict([static_bp, bucket, bucket1, bucket_policy1, bucket2, bucket_policy2, bucket3, bucket_policy3])) + def test_s3_additional_bucket_with_custom_policy(self): + conf_under_test = { + 'buckets': [ + { + 'name': 'testbucket', + 'policy': 'tests/sample-s3-additional-bucket-custom-policy.json' + } + ] + } + + # Resources to compare + expected_bucket_policy = [ + { + 'Action': ['s3:Get*', 's3:Put*', 's3:List*', 's3:Delete*'], + 'Resource': 'arn:aws:s3:::testbucket/*', + 'Effect': 'Allow', + 'Principal': {'AWS': '*'} + } + ] + + project_config = ProjectConfig('tests/sample-project.yaml', 'dev') + project_config.config['s3'] = conf_under_test + config = ConfigParser(project_config.config, 'my-stack-name') + + template = Template() + config.s3(template) + resources = template.resources.values() + + s3_cfg = self._resources_to_dict(resources) + s3_bucket_policy = s3_cfg['testbucketPolicy']['Properties']['PolicyDocument']['Statement'] + + compare(expected_bucket_policy, s3_bucket_policy) + + def test_s3_additional_bucket_with_multi_part_custom_policy(self): + conf_under_test = { + 'buckets': [ + { + 'name': 'testbucket', + 'policy': 'tests/sample-s3-additional-bucket-multi-part-custom-policy.json' + } + ] + } + + # Resources to compare + expected_bucket_policy = [ + { + 'Action': ['s3:Get*', 's3:Put*', 's3:List*', 's3:Delete*'], + 'Resource': 'arn:aws:s3:::testbucket/*', + 'Effect': 'Allow', + 'Condition': { + 'StringEquals': { + 'aws:sourceVpc': {'Ref': 'VPC'} + } + } + }, + { + 'Action': ['s3:Put*'], + 'Resource': 'arn:aws:s3:::testbucket/*', + 'Effect': 'Deny', + 'Condition': { + 'StringNotEquals': { + 's3:x-amz-server-side-encryption': 'AES256' + } + } + } + ] + + project_config = ProjectConfig('tests/sample-project.yaml', 'dev') + project_config.config['s3'] = conf_under_test + config = ConfigParser(project_config.config, 'my-stack-name') + + template = Template() + config.s3(template) + resources = template.resources.values() + + s3_cfg = self._resources_to_dict(resources) + s3_bucket_policy = s3_cfg['testbucketPolicy']['Properties']['PolicyDocument']['Statement'] + + compare(expected_bucket_policy, s3_bucket_policy) + def test_rds(self): template = Template()