Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions src/e3/aws/troposphere/s3/__init__.py
Original file line number Diff line number Diff line change
@@ -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,
]
195 changes: 195 additions & 0 deletions tests/tests_e3_aws/troposphere/s3/bucket-with-roles.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
19 changes: 19 additions & 0 deletions tests/tests_e3_aws/troposphere/s3/s3_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down