From 1d852c359e518a530bff635bc10852527f88b3d2 Mon Sep 17 00:00:00 2001 From: Pierre Trespeuch Date: Thu, 23 Jun 2022 17:01:22 +0200 Subject: [PATCH] Add BucketWithRoles construct This construct provides a bucket and its associated access roles and policies. TN: V620-043 --- src/e3/aws/troposphere/s3/__init__.py | 102 +++++++++ .../troposphere/s3/bucket-with-roles.json | 195 ++++++++++++++++++ tests/tests_e3_aws/troposphere/s3/s3_test.py | 19 ++ 3 files changed, 316 insertions(+) create mode 100644 tests/tests_e3_aws/troposphere/s3/bucket-with-roles.json diff --git a/src/e3/aws/troposphere/s3/__init__.py b/src/e3/aws/troposphere/s3/__init__.py index e69de29b..a24643cc 100644 --- a/src/e3/aws/troposphere/s3/__init__.py +++ b/src/e3/aws/troposphere/s3/__init__.py @@ -0,0 +1,102 @@ +"""Provide s3 high level constructs.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +from e3.aws.troposphere import Construct +from e3.aws.troposphere.iam.managed_policy import ManagedPolicy +from e3.aws.troposphere.iam.policy_statement import Allow, Trust +from e3.aws.troposphere.iam.role import Role +from e3.aws.troposphere.s3.bucket import Bucket + +if TYPE_CHECKING: + from typing import Any, Union + from troposphere import AWSObject, Stack + + +class BucketWithRoles(Construct): + """Provide resources for a s3 bucket with its access roles.""" + + def __init__( + self, + name: str, + iam_names_prefix: str, + iam_path: str, + trusted_accounts: list[str], + iam_read_root_name: str = "Read", + iam_write_root_name: str = "Write", + **bucket_kwargs: Any, + ) -> None: + """Initialize BucketWithRoles instance. + + :param name: name of the bucket + :param iam_names_prefix: prefix for policies and roles names + :param iam_path: path for iam resources + :param trusted_accounts: accounts to be trusted by access roles + :param iam_read_root_name: root name for read access roles and policy + :param iam_write_root_name: root name for write access roles and policy + :param bucket_kwargs: keyword arguments to pass to the bucket constructor + """ + self.name = name + self.iam_names_prefix = iam_names_prefix + self.trusted_accounts = trusted_accounts + + self.bucket = Bucket(name=self.name, **bucket_kwargs) + self.read_policy = ManagedPolicy( + name=f"{self.iam_names_prefix}{iam_read_root_name}Policy", + description=f"Grants read access permissions to {self.name} bucket", + statements=[ + Allow(action=["s3:GetObject"], resource=self.bucket.all_objects_arn), + Allow(action=["s3:ListBucket"], resource=self.bucket.arn), + ], + path=iam_path, + ) + self.read_role = Role( + name=f"{self.iam_names_prefix}{iam_read_root_name}Role", + description=f"Role with read access to {self.name} bucket.", + trust=Trust(accounts=self.trusted_accounts), + managed_policy_arns=[self.read_policy.arn], + path=iam_path, + ) + self.push_policy = ManagedPolicy( + name=f"{self.iam_names_prefix}{iam_write_root_name}Policy", + description=f"Grants write access permissions to {self.name} bucket", + statements=[ + Allow( + action=["s3:PutObject", "s3:DeleteObject"], + resource=self.bucket.all_objects_arn, + ) + ], + path=iam_path, + ) + self.push_role = Role( + name=f"{self.iam_names_prefix}{iam_write_root_name}Role", + description=f"Role with read and write access to {self.name} bucket.", + trust=Trust(accounts=self.trusted_accounts), + managed_policy_arns=[self.push_policy.arn, self.read_policy.arn], + path=iam_path, + ) + + @property + def ref(self): + """Return bucket ref.""" + return self.bucket.ref + + @property + def arn(self): + """Return bucket arn.""" + return self.bucket.arn + + @property + def all_objects_arn(self): + return self.bucket.all_objects_arn + + def resources(self, stack: Stack) -> list[Union[AWSObject, Construct]]: + """Return resources associated with the construct.""" + return [ + self.bucket, + self.read_policy, + self.read_role, + self.push_policy, + self.push_role, + ] diff --git a/tests/tests_e3_aws/troposphere/s3/bucket-with-roles.json b/tests/tests_e3_aws/troposphere/s3/bucket-with-roles.json new file mode 100644 index 00000000..0fe369d4 --- /dev/null +++ b/tests/tests_e3_aws/troposphere/s3/bucket-with-roles.json @@ -0,0 +1,195 @@ +{ + "TestBucketWithRoles": { + "Properties": { + "BucketName": "test-bucket-with-roles", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "Type": "AWS::S3::Bucket" + }, + "TestBucketWithRolesPolicy": { + "Properties": { + "Bucket": { + "Ref": "TestBucketWithRoles" + }, + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Action": "s3:*", + "Resource": "arn:aws:s3:::test-bucket-with-roles/*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + } + }, + { + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Action": "s3:PutObject", + "Resource": "arn:aws:s3:::test-bucket-with-roles/*", + "Condition": { + "StringNotEquals": { + "s3:x-amz-server-side-encryption": "AES256" + } + } + }, + { + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Action": "s3:PutObject", + "Resource": "arn:aws:s3:::test-bucket-with-roles/*", + "Condition": { + "Null": { + "s3:x-amz-server-side-encryption": "true" + } + } + } + ] + } + }, + "Type": "AWS::S3::BucketPolicy" + }, + "TestBucketRestorePolicy": { + "Properties": { + "Description": "Grants read access permissions to test-bucket-with-roles bucket", + "ManagedPolicyName": "TestBucketRestorePolicy", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:GetObject" + ], + "Resource": "arn:aws:s3:::test-bucket-with-roles/*" + }, + { + "Effect": "Allow", + "Action": [ + "s3:ListBucket" + ], + "Resource": "arn:aws:s3:::test-bucket-with-roles" + } + ] + }, + "Path": "/test/" + }, + "Type": "AWS::IAM::ManagedPolicy" + }, + "TestBucketRestoreRole": { + "Properties": { + "RoleName": "TestBucketRestoreRole", + "Description": "Role with read access to test-bucket-with-roles bucket.", + "ManagedPolicyArns": [ + { + "Ref": "TestBucketRestorePolicy" + } + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Principal": { + "AWS": [ + "arn:aws:iam::123456789:root" + ] + } + } + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestBucketRestoreRole" + } + ], + "Path": "/test/" + }, + "Type": "AWS::IAM::Role" + }, + "TestBucketPushPolicy": { + "Properties": { + "Description": "Grants write access permissions to test-bucket-with-roles bucket", + "ManagedPolicyName": "TestBucketPushPolicy", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:PutObject", + "s3:DeleteObject" + ], + "Resource": "arn:aws:s3:::test-bucket-with-roles/*" + } + ] + }, + "Path": "/test/" + }, + "Type": "AWS::IAM::ManagedPolicy" + }, + "TestBucketPushRole": { + "Properties": { + "RoleName": "TestBucketPushRole", + "Description": "Role with read and write access to test-bucket-with-roles bucket.", + "ManagedPolicyArns": [ + { + "Ref": "TestBucketPushPolicy" + }, + { + "Ref": "TestBucketRestorePolicy" + } + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Principal": { + "AWS": [ + "arn:aws:iam::123456789:root" + ] + } + } + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestBucketPushRole" + } + ], + "Path": "/test/" + }, + "Type": "AWS::IAM::Role" + } +} diff --git a/tests/tests_e3_aws/troposphere/s3/s3_test.py b/tests/tests_e3_aws/troposphere/s3/s3_test.py index 3b5cd108..e71de19b 100644 --- a/tests/tests_e3_aws/troposphere/s3/s3_test.py +++ b/tests/tests_e3_aws/troposphere/s3/s3_test.py @@ -3,6 +3,7 @@ import os from e3.aws.troposphere.s3.bucket import Bucket, EncryptionAlgorithm +from e3.aws.troposphere.s3 import BucketWithRoles from e3.aws.troposphere import Stack from e3.aws.troposphere.awslambda import Py38Function from e3.aws.troposphere.sns import Topic @@ -49,6 +50,24 @@ def test_bucket(stack: Stack) -> None: assert stack.export()["Resources"] == expected_template +def test_bucket_with_roles(stack: Stack) -> None: + """Test BucketWithRoles.""" + bucket = BucketWithRoles( + name="test-bucket-with-roles", + iam_names_prefix="TestBucket", + iam_read_root_name="Restore", + iam_write_root_name="Push", + iam_path="/test/", + trusted_accounts=["123456789"], + ) + stack.add(bucket) + + with open(os.path.join(TEST_DIR, "bucket-with-roles.json")) as fd: + expected_template = json.load(fd) + + assert stack.export()["Resources"] == expected_template + + def test_bucket_multi_encryption(stack: Stack) -> None: """Test bucket accepting multiple types of encryptions and without default.""" bucket = Bucket(